/* eslint-disable prefer-spread */
/* eslint-disable no-use-before-define */
/* eslint-disable no-mixed-operators */
/* eslint-disable no-self-assign */
/* eslint-disable prefer-destructuring */
/* eslint-disable no-multi-assign */
/* eslint-disable no-restricted-globals */
/* eslint-disable new-cap */
/* eslint-disable prefer-const */
/* eslint-disable no-tabs */
/* eslint-disable no-param-reassign */
/* eslint-disable no-shadow */
/* eslint-disable no-lonely-if */
/* eslint-disable func-style */
/* eslint-disable max-len */
/**
 * @license jCanvas v21.0.1
 * Copyright 2017 Caleb Evans
 * Released under the MIT license
 */
;(function(jQuery, global, factory) {
  if (typeof module === 'object' && typeof module.exports === 'object') {
    module.exports = function(jQuery, w) {
      return factory(jQuery, w)
    }
  } else {
    factory(jQuery, global)
  }

  // Pass this if window is not defined yet
})(
  typeof window !== 'undefined' ? window.jQuery : {},
  typeof window !== 'undefined' ? window : this,
  ($, window) => {
    const { document } = window
    const { Image } = window
    const { Array } = window
    const { getComputedStyle } = window
    const { Math } = window
    const { Number } = window
    const { parseFloat } = window

    // Define local aliases to frequently used properties
    let defaults
    // Aliases to jQuery methods
    const extendObject = $.extend
    const { inArray } = $
    const typeOf = function(operand) {
      return Object.prototype.toString
        .call(operand)
        .slice(8, -1)
        .toLowerCase()
    }
    const { isPlainObject } = $
    // Math constants and functions
    const { PI } = Math
    const { round } = Math
    const { abs } = Math
    const { sin } = Math
    const { cos } = Math
    const { atan2 } = Math
    // The Array slice() method
    const arraySlice = Array.prototype.slice
    // jQuery's internal event normalization function
    const jQueryEventFix = $.event.fix
    // Object for storing a number of internal property maps
    const maps = {}
    // jQuery internal caches
    const caches = {
      dataCache: {},
      propCache: {},
      imageCache: {}
    }
    // Base transformations
    const baseTransforms = {
      rotate: 0,
      scaleX: 1,
      scaleY: 1,
      translateX: 0,
      translateY: 0,
      // Store all previous masks
      masks: []
    }
    // Object for storing CSS-related properties
    const css = {}
    const tangibleEvents = [
      'mousedown',
      'mousemove',
      'mouseup',
      'mouseover',
      'mouseout',
      'touchstart',
      'touchmove',
      'touchend'
    ]

    // Constructor for creating objects that inherit from jCanvas preferences and defaults
    function jCanvasObject(args) {
      const params = this
      let propName
      // Copy the given parameters into new object
      // eslint-disable-next-line no-restricted-syntax
      for (propName in args) {
        // Do not merge defaults into parameters
        if (Object.prototype.hasOwnProperty.call(args, propName)) {
          params[propName] = args[propName]
        }
      }
      return params
    }

    // jCanvas object in which global settings are other data are stored
    const jCanvas = {
      // Events object for storing jCanvas event initiation functions
      events: {},
      // Object containing all jCanvas event hooks
      eventHooks: {},
      // Settings for enabling future jCanvas features
      future: {}
    }

    // jCanvas default property values
    function jCanvasDefaults() {
      extendObject(this, jCanvasDefaults.baseDefaults)
    }
    jCanvasDefaults.baseDefaults = {
      align: 'center',
      arrowAngle: 90,
      arrowRadius: 0,
      autosave: true,
      baseline: 'middle',
      bringToFront: false,
      ccw: false,
      closed: false,
      compositing: 'source-over',
      concavity: 0,
      cornerRadius: 0,
      count: 1,
      cropFromCenter: true,
      crossOrigin: null,
      cursors: null,
      disableEvents: false,
      draggable: false,
      dragGroups: null,
      groups: null,
      data: null,
      dx: null,
      dy: null,
      end: 360,
      eventX: null,
      eventY: null,
      fillStyle: 'transparent',
      fontStyle: 'normal',
      fontSize: '12pt',
      fontFamily: 'sans-serif',
      fromCenter: true,
      height: null,
      imageSmoothing: true,
      inDegrees: true,
      intangible: false,
      index: null,
      letterSpacing: null,
      lineHeight: 1,
      layer: false,
      mask: false,
      maxWidth: null,
      miterLimit: 10,
      name: null,
      opacity: 1,
      r1: null,
      r2: null,
      radius: 0,
      repeat: 'repeat',
      respectAlign: false,
      restrictDragToAxis: null,
      rotate: 0,
      rounded: false,
      scale: 1,
      scaleX: 1,
      scaleY: 1,
      shadowBlur: 0,
      shadowColor: 'transparent',
      shadowStroke: false,
      shadowX: 0,
      shadowY: 0,
      sHeight: null,
      sides: 0,
      source: '',
      spread: 0,
      start: 0,
      strokeCap: 'butt',
      strokeDash: null,
      strokeDashOffset: 0,
      strokeJoin: 'miter',
      strokeStyle: 'transparent',
      strokeWidth: 1,
      sWidth: null,
      sx: null,
      sy: null,
      text: '',
      translate: 0,
      translateX: 0,
      translateY: 0,
      type: null,
      visible: true,
      width: null,
      x: 0,
      y: 0
    }
    defaults = new jCanvasDefaults()
    jCanvasObject.prototype = defaults

    /* Internal helper methods */

    // Determines if the given operand is a string
    function isString(operand) {
      return typeOf(operand) === 'string'
    }

    // Determines if the given operand is a function
    function isFunction(operand) {
      return typeOf(operand) === 'function'
    }

    // Determines if the given operand is numeric
    function isNumeric(operand) {
      return !isNaN(Number(operand)) && !isNaN(parseFloat(operand))
    }

    // Get 2D context for the given canvas
    function _getContext(canvas) {
      return canvas && canvas.getContext ? canvas.getContext('2d') : null
    }

    // Coerce designated number properties from strings to numbers
    function _coerceNumericProps(props) {
      let propName
      let propType
      let propValue
      // Loop through all properties in given property map
      for (propName in props) {
        if (Object.prototype.hasOwnProperty.call(props, propName)) {
          propValue = props[propName]
          propType = typeOf(propValue)
          // If property is non-empty string and value is numeric
          if (propType === 'string' && isNumeric(propValue) && propName !== 'text') {
            // Convert value to number
            props[propName] = parseFloat(propValue)
          }
        }
      }
      // Ensure value of text property is always a string
      if (props.text !== undefined) {
        props.text = String(props.text)
      }
    }

    // Clone the given transformations object
    function _cloneTransforms(transforms) {
      // Clone the object itself
      transforms = extendObject({}, transforms)
      // Clone the object's masks array
      transforms.masks = transforms.masks.slice(0)
      return transforms
    }

    // Save canvas context and update transformation stack
    function _saveCanvas(ctx, data) {
      let transforms
      ctx.save()
      transforms = _cloneTransforms(data.transforms)
      data.savedTransforms.push(transforms)
    }

    // Restore canvas context update transformation stack
    function _restoreCanvas(ctx, data) {
      if (data.savedTransforms.length === 0) {
        // Reset transformation state if it can't be restored any more
        data.transforms = _cloneTransforms(baseTransforms)
      } else {
        // Restore canvas context
        ctx.restore()
        // Restore current transform state to the last saved state
        data.transforms = data.savedTransforms.pop()
      }
    }

    // Set the style with the given name
    function _setStyle(canvas, ctx, params, styleName) {
      if (params[styleName]) {
        if (isFunction(params[styleName])) {
          // Handle functions
          ctx[styleName] = params[styleName].call(canvas, params)
        } else {
          // Handle string values
          ctx[styleName] = params[styleName]
        }
      }
    }

    // Set canvas context properties
    function _setGlobalProps(canvas, ctx, params) {
      _setStyle(canvas, ctx, params, 'fillStyle')
      _setStyle(canvas, ctx, params, 'strokeStyle')
      ctx.lineWidth = params.strokeWidth
      // Optionally round corners for paths
      if (params.rounded) {
        ctx.lineCap = ctx.lineJoin = 'round'
      } else {
        ctx.lineCap = params.strokeCap
        ctx.lineJoin = params.strokeJoin
        ctx.miterLimit = params.miterLimit
      }
      // Reset strokeDash if null
      if (!params.strokeDash) {
        params.strokeDash = []
      }
      // Dashed lines
      if (ctx.setLineDash) {
        ctx.setLineDash(params.strokeDash)
      }
      ctx.webkitLineDash = params.strokeDash
      ctx.lineDashOffset = ctx.webkitLineDashOffset = ctx.mozDashOffset = params.strokeDashOffset
      // Drop shadow
      ctx.shadowOffsetX = params.shadowX
      ctx.shadowOffsetY = params.shadowY
      ctx.shadowBlur = params.shadowBlur
      ctx.shadowColor = params.shadowColor
      // Opacity and composite operation
      ctx.globalAlpha = params.opacity
      ctx.globalCompositeOperation = params.compositing
      // Support cross-browser toggling of image smoothing
      if (params.imageSmoothing) {
        ctx.imageSmoothingEnabled = params.imageSmoothing
      }
    }

    // Optionally enable masking support for this path
    function _enableMasking(ctx, data, params) {
      if (params.mask) {
        // If jCanvas autosave is enabled
        if (params.autosave) {
          // Automatically save transformation state by default
          _saveCanvas(ctx, data)
        }
        // Clip the current path
        ctx.clip()
        // Keep track of current masks
        data.transforms.masks.push(params._args)
      }
    }

    // Restore individual shape transformation
    function _restoreTransform(ctx, params) {
      // If shape has been transformed by jCanvas
      if (params._transformed) {
        // Restore canvas context
        ctx.restore()
      }
    }

    // Close current canvas path
    function _closePath(canvas, ctx, params) {
      let data

      // Optionally close path
      if (params.closed) {
        ctx.closePath()
      }

      if (params.shadowStroke && params.strokeWidth !== 0) {
        // Extend the shadow to include the stroke of a drawing

        // Add a stroke shadow by stroking before filling
        ctx.stroke()
        ctx.fill()
        // Ensure the below stroking does not inherit a shadow
        ctx.shadowColor = 'transparent'
        ctx.shadowBlur = 0
        // Stroke over fill as usual
        ctx.stroke()
      } else {
        // If shadowStroke is not enabled, stroke & fill as usual

        ctx.fill()
        // Prevent extra shadow created by stroke (but only when fill is present)
        if (params.fillStyle !== 'transparent') {
          ctx.shadowColor = 'transparent'
        }
        if (params.strokeWidth !== 0) {
          // Only stroke if the stroke is not 0
          ctx.stroke()
        }
      }

      // Optionally close path
      if (!params.closed) {
        ctx.closePath()
      }

      // Restore individual shape transformation
      _restoreTransform(ctx, params)

      // Mask shape if chosen
      if (params.mask) {
        // Retrieve canvas data
        data = _getCanvasData(canvas)
        _enableMasking(ctx, data, params)
      }
    }

    // Transform (translate, scale, or rotate) shape
    function _transformShape(canvas, ctx, params, width, height) {
      // Get conversion factor for radians
      params._toRad = params.inDegrees ? PI / 180 : 1

      params._transformed = true
      ctx.save()

      // Optionally measure (x, y) position from top-left corner
      if (!params.fromCenter && !params._centered && width !== undefined) {
        // Always draw from center unless otherwise specified
        if (height === undefined) {
          height = width
        }
        params.x += width / 2
        params.y += height / 2
        params._centered = true
      }
      // Optionally rotate shape
      if (params.rotate) {
        _rotateCanvas(ctx, params, null)
      }
      // Optionally scale shape
      if (params.scale !== 1 || params.scaleX !== 1 || params.scaleY !== 1) {
        _scaleCanvas(ctx, params, null)
      }
      // Optionally translate shape
      if (params.translate || params.translateX || params.translateY) {
        _translateCanvas(ctx, params, null)
      }
    }

    /* Plugin API */

    // Extend jCanvas with a user-defined method
    jCanvas.extend = function extend(plugin) {
      // Create plugin
      if (plugin.name) {
        // Merge properties with defaults
        if (plugin.props) {
          extendObject(defaults, plugin.props)
        }
        // Define plugin method
        $.fn[plugin.name] = function self(args) {
          const $canvases = this
          let canvas
          let e
          let ctx
          let params

          for (e = 0; e < $canvases.length; e += 1) {
            canvas = $canvases[e]
            ctx = _getContext(canvas)
            if (ctx) {
              params = new jCanvasObject(args)
              _addLayer(canvas, params, args, self)

              _setGlobalProps(canvas, ctx, params)
              plugin.fn.call(canvas, ctx, params)
            }
          }
          return $canvases
        }
        // Add drawing type to drawing map
        if (plugin.type) {
          maps.drawings[plugin.type] = plugin.name
        }
      }
      return $.fn[plugin.name]
    }

    /* Layer API */

    // Retrieved the stored jCanvas data for a canvas element
    function _getCanvasData(canvas) {
      const { dataCache } = caches
      let data
      if (dataCache._canvas === canvas && dataCache._data) {
        // Retrieve canvas data from cache if possible
        data = dataCache._data
      } else {
        // Retrieve canvas data from jQuery's internal data storage
        data = $.data(canvas, 'jCanvas')
        if (!data) {
          // Create canvas data object if it does not already exist
          data = {
            // The associated canvas element
            canvas,
            // Layers array
            layers: [],
            // Layer maps
            layer: {
              names: {},
              groups: {}
            },
            eventHooks: {},
            // All layers that intersect with the event coordinates (regardless of visibility)
            intersecting: [],
            // The topmost layer whose area contains the event coordinates
            lastIntersected: null,
            cursor: $(canvas).css('cursor'),
            // Properties for the current drag event
            drag: {
              layer: null,
              dragging: false
            },
            // Data for the current event
            event: {
              type: null,
              x: null,
              y: null
            },
            // Events which already have been bound to the canvas
            events: {},
            // The canvas's current transformation state
            transforms: _cloneTransforms(baseTransforms),
            savedTransforms: [],
            // Whether a layer is being animated or not
            animating: false,
            // The layer currently being animated
            animated: null,
            // The device pixel ratio
            pixelRatio: 1,
            // Whether pixel ratio transformations have been applied
            scaled: false,
            // Whether the canvas should be redrawn when a layer mousemove
            // event triggers (either directly, or indirectly via dragging)
            redrawOnMousemove: false
          }
          // Use jQuery to store canvas data
          $.data(canvas, 'jCanvas', data)
        }
        // Cache canvas data for faster retrieval
        dataCache._canvas = canvas
        dataCache._data = data
      }
      return data
    }

    // Initialize all of a layer's associated jCanvas events
    function _addLayerEvents($canvas, data, layer) {
      let eventName
      // Determine which jCanvas events need to be bound to this layer
      for (eventName in jCanvas.events) {
        if (Object.prototype.hasOwnProperty.call(jCanvas.events, eventName)) {
          // If layer has callback function to complement it
          if (layer[eventName] || (layer.cursors && layer.cursors[eventName])) {
            // Bind event to layer
            _addExplicitLayerEvent($canvas, data, layer, eventName)
          }
        }
      }
      if (!data.events.mouseout) {
        $canvas.bind('mouseout.jCanvas', () => {
          // Retrieve the layer whose drag event was canceled
          let { layer } = data.drag
          let l
          // If cursor mouses out of canvas while dragging
          if (layer) {
            // Cancel drag
            data.drag = {}
            _triggerLayerEvent($canvas, data, layer, 'dragcancel')
          }
          // Loop through all layers
          for (l = 0; l < data.layers.length; l += 1) {
            layer = data.layers[l]
            // If layer thinks it's still being moused over
            if (layer._hovered) {
              // Trigger mouseout on layer
              $canvas.triggerLayerEvent(data.layers[l], 'mouseout')
            }
          }
          // Redraw layers
          $canvas.drawLayers()
        })
        // Indicate that an event handler has been bound
        data.events.mouseout = true
      }
    }

    // Initialize the given event on the given layer
    function _addLayerEvent($canvas, data, layer, eventName) {
      // Use touch events if appropriate
      // eventName = _getMouseEventName(eventName);
      // Bind event to layer
      jCanvas.events[eventName]($canvas, data)
      layer._event = true
    }

    // Add a layer event that was explicitly declared in the layer's parameter map,
    // excluding events added implicitly (e.g. mousemove event required by draggable
    // layers)
    function _addExplicitLayerEvent($canvas, data, layer, eventName) {
      _addLayerEvent($canvas, data, layer, eventName)
      if (eventName === 'mouseover' || eventName === 'mouseout' || eventName === 'mousemove') {
        data.redrawOnMousemove = true
      }
    }

    // Enable drag support for this layer
    function _enableDrag($canvas, data, layer) {
      let dragHelperEvents
      let eventName
      let i
      // Only make layer draggable if necessary
      if (layer.draggable || layer.cursors) {
        // Organize helper events which enable drag support
        dragHelperEvents = ['mousedown', 'mousemove', 'mouseup']

        // Bind each helper event to the canvas
        for (i = 0; i < dragHelperEvents.length; i += 1) {
          // Use touch events if appropriate
          eventName = dragHelperEvents[i]
          // Bind event
          _addLayerEvent($canvas, data, layer, eventName)
        }
        // Indicate that this layer has events bound to it
        layer._event = true
      }
    }

    // Update a layer property map if property is changed
    function _updateLayerName($canvas, data, layer, props) {
      const nameMap = data.layer.names

      // If layer name is being added, not changed
      if (!props) {
        props = layer
      } else {
        // Remove old layer name entry because layer name has changed
        if (props.name !== undefined && isString(layer.name) && layer.name !== props.name) {
          delete nameMap[layer.name]
        }
      }

      // Add new entry to layer name map with new name
      if (isString(props.name)) {
        nameMap[props.name] = layer
      }
    }

    // Create or update the data map for the given layer and group type
    function _updateLayerGroups($canvas, data, layer, props) {
      const groupMap = data.layer.groups
      let group
      let groupName
      let g
      let index
      let l

      // If group name is not changing
      if (!props) {
        props = layer
      } else {
        // Remove layer from all of its associated groups
        if (props.groups !== undefined && layer.groups !== null) {
          for (g = 0; g < layer.groups.length; g += 1) {
            groupName = layer.groups[g]
            group = groupMap[groupName]
            if (group) {
              // Remove layer from its old layer group entry
              for (l = 0; l < group.length; l += 1) {
                if (group[l] === layer) {
                  // Keep track of the layer's initial index
                  index = l
                  // Remove layer once found
                  group.splice(l, 1)
                  break
                }
              }
              // Remove layer group entry if group is empty
              if (group.length === 0) {
                delete groupMap[groupName]
              }
            }
          }
        }
      }

      // Add layer to new group if a new group name is given
      if (props.groups !== undefined && props.groups !== null) {
        for (g = 0; g < props.groups.length; g += 1) {
          groupName = props.groups[g]

          group = groupMap[groupName]
          if (!group) {
            // Create new group entry if it doesn't exist
            group = groupMap[groupName] = []
            group.name = groupName
          }
          if (index === undefined) {
            // Add layer to end of group unless otherwise stated
            index = group.length
          }
          // Add layer to its new layer group
          group.splice(index, 0, layer)
        }
      }
    }

    // Get event hooks object for the first selected canvas
    $.fn.getEventHooks = function getEventHooks() {
      const $canvases = this
      let canvas
      let data
      let eventHooks = {}

      if ($canvases.length !== 0) {
        canvas = $canvases[0]
        data = _getCanvasData(canvas)
        eventHooks = data.eventHooks
      }
      return eventHooks
    }

    // Set event hooks for the selected canvases
    $.fn.setEventHooks = function setEventHooks(eventHooks) {
      const $canvases = this
      let e
      let data
      for (e = 0; e < $canvases.length; e += 1) {
        data = _getCanvasData($canvases[e])
        extendObject(data.eventHooks, eventHooks)
      }
      return $canvases
    }

    // Get jCanvas layers array
    $.fn.getLayers = function getLayers(callback) {
      const $canvases = this
      let canvas
      let data
      let layers
      let layer
      let l
      let matching = []

      if ($canvases.length !== 0) {
        canvas = $canvases[0]
        data = _getCanvasData(canvas)
        // Retrieve layers array for this canvas
        layers = data.layers

        // If a callback function is given
        if (isFunction(callback)) {
          // Filter the layers array using the callback
          for (l = 0; l < layers.length; l += 1) {
            layer = layers[l]
            if (callback.call(canvas, layer)) {
              // Add layer to array of matching layers if test passes
              matching.push(layer)
            }
          }
        } else {
          // Otherwise, get all layers

          matching = layers
        }
      }
      return matching
    }

    // Get a single jCanvas layer object
    $.fn.getLayer = function getLayer(layerId) {
      const $canvases = this
      let canvas
      let data
      let layers
      let layer
      let l
      let idType

      if ($canvases.length !== 0) {
        canvas = $canvases[0]
        data = _getCanvasData(canvas)
        layers = data.layers
        idType = typeOf(layerId)

        if (layerId && layerId.layer) {
          // Return the actual layer object if given
          layer = layerId
        } else if (idType === 'number') {
          // Retrieve the layer using the given index

          // Allow for negative indices
          if (layerId < 0) {
            layerId = layers.length + layerId
          }
          // Get layer with the given index
          layer = layers[layerId]
        } else if (idType === 'regexp') {
          // Get layer with the name that matches the given regex
          for (l = 0; l < layers.length; l += 1) {
            // Check if layer matches name
            if (isString(layers[l].name) && layers[l].name.match(layerId)) {
              layer = layers[l]
              break
            }
          }
        } else {
          // Get layer with the given name
          layer = data.layer.names[layerId]
        }
      }
      return layer
    }

    // Get all layers in the given group
    $.fn.getLayerGroup = function getLayerGroup(groupId) {
      const $canvases = this
      let canvas
      let data
      let groups
      let groupName
      let group
      const idType = typeOf(groupId)

      if ($canvases.length !== 0) {
        canvas = $canvases[0]

        if (idType === 'array') {
          // Return layer group if given
          group = groupId
        } else if (idType === 'regexp') {
          // Get canvas data
          data = _getCanvasData(canvas)
          groups = data.layer.groups
          // Loop through all layers groups for this canvas
          for (groupName in groups) {
            // Find a group whose name matches the given regex
            if (groupName.match(groupId)) {
              group = groups[groupName]
              // Stop after finding the first matching group
              break
            }
          }
        } else {
          // Find layer group with the given group name
          data = _getCanvasData(canvas)
          group = data.layer.groups[groupId]
        }
      }
      return group
    }

    // Get index of layer in layers array
    $.fn.getLayerIndex = function getLayerIndex(layerId) {
      const $canvases = this
      const layers = $canvases.getLayers()
      const layer = $canvases.getLayer(layerId)

      return inArray(layer, layers)
    }

    // Set properties of a layer
    $.fn.setLayer = function setLayer(layerId, props) {
      const $canvases = this
      let $canvas
      let e
      let data
      let layer
      let propName
      let propValue
      let propType

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        data = _getCanvasData($canvases[e])

        layer = $($canvases[e]).getLayer(layerId)
        if (layer) {
          // Update layer property maps
          _updateLayerName($canvas, data, layer, props)
          _updateLayerGroups($canvas, data, layer, props)

          _coerceNumericProps(props)

          // Merge properties with layer
          for (propName in props) {
            if (Object.prototype.hasOwnProperty.call(props, propName)) {
              propValue = props[propName]
              propType = typeOf(propValue)
              if (propType === 'object' && isPlainObject(propValue)) {
                // Clone objects
                layer[propName] = extendObject({}, propValue)
                _coerceNumericProps(layer[propName])
              } else if (propType === 'array') {
                // Clone arrays
                layer[propName] = propValue.slice(0)
              } else if (propType === 'string') {
                if (propValue.indexOf('+=') === 0) {
                  // Increment numbers prefixed with +=
                  layer[propName] += parseFloat(propValue.substr(2))
                } else if (propValue.indexOf('-=') === 0) {
                  // Decrement numbers prefixed with -=
                  layer[propName] -= parseFloat(propValue.substr(2))
                } else if (!isNaN(propValue) && isNumeric(propValue) && propName !== 'text') {
                  // Convert numeric values as strings to numbers
                  layer[propName] = parseFloat(propValue)
                } else {
                  // Otherwise, set given string value
                  layer[propName] = propValue
                }
              } else {
                // Otherwise, set given value
                layer[propName] = propValue
              }
            }
          }

          // Update layer events
          _addLayerEvents($canvas, data, layer)
          _enableDrag($canvas, data, layer)

          // If layer's properties were changed
          if ($.isEmptyObject(props) === false) {
            _triggerLayerEvent($canvas, data, layer, 'change', props)
          }
        }
      }
      return $canvases
    }

    // Set properties of all layers (optionally filtered by a callback)
    $.fn.setLayers = function setLayers(props, callback) {
      const $canvases = this
      let $canvas
      let e
      let layers
      let l
      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])

        layers = $canvas.getLayers(callback)
        // Loop through all layers
        for (l = 0; l < layers.length; l += 1) {
          // Set properties of each layer
          $canvas.setLayer(layers[l], props)
        }
      }
      return $canvases
    }

    // Set properties of all layers in the given group
    $.fn.setLayerGroup = function setLayerGroup(groupId, props) {
      const $canvases = this
      let $canvas
      let e
      let group
      let l

      for (e = 0; e < $canvases.length; e += 1) {
        // Get layer group
        $canvas = $($canvases[e])

        group = $canvas.getLayerGroup(groupId)
        // If group exists
        if (group) {
          // Loop through layers in group
          for (l = 0; l < group.length; l += 1) {
            // Merge given properties with layer
            $canvas.setLayer(group[l], props)
          }
        }
      }
      return $canvases
    }

    // Move a layer to the given index in the layers array
    $.fn.moveLayer = function moveLayer(layerId, index) {
      const $canvases = this
      let $canvas
      let e
      let data
      let layers
      let layer

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        data = _getCanvasData($canvases[e])

        // Retrieve layers array and desired layer
        layers = data.layers
        layer = $canvas.getLayer(layerId)
        if (layer) {
          // Ensure layer index is accurate
          layer.index = inArray(layer, layers)

          // Remove layer from its current placement
          layers.splice(layer.index, 1)
          // Add layer in its new placement
          layers.splice(index, 0, layer)

          // Handle negative indices
          if (index < 0) {
            index = layers.length + index
          }
          // Update layer's stored index
          layer.index = index

          _triggerLayerEvent($canvas, data, layer, 'move')
        }
      }
      return $canvases
    }

    // Remove a jCanvas layer
    $.fn.removeLayer = function removeLayer(layerId) {
      const $canvases = this
      let $canvas
      let e
      let data
      let layers
      let layer

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        data = _getCanvasData($canvases[e])

        // Retrieve layers array and desired layer
        layers = $canvas.getLayers()
        layer = $canvas.getLayer(layerId)
        // Remove layer if found
        if (layer) {
          // Ensure layer index is accurate
          layer.index = inArray(layer, layers)
          // Remove layer and allow it to be re-added later
          layers.splice(layer.index, 1)
          delete layer._layer

          // Update layer name map
          _updateLayerName($canvas, data, layer, {
            name: null
          })
          // Update layer group map
          _updateLayerGroups($canvas, data, layer, {
            groups: null
          })

          // Trigger 'remove' event
          _triggerLayerEvent($canvas, data, layer, 'remove')
        }
      }
      return $canvases
    }

    // Remove all layers
    $.fn.removeLayers = function removeLayers(callback) {
      const $canvases = this
      let $canvas
      let e
      let data
      let layers
      let layer
      let l

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        data = _getCanvasData($canvases[e])
        layers = $canvas.getLayers(callback).slice(0)
        // Remove all layers individually
        for (l = 0; l < layers.length; l += 1) {
          layer = layers[l]
          $canvas.removeLayer(layer)
        }
        // Update layer maps
        data.layer.names = {}
        data.layer.groups = {}
      }
      return $canvases
    }

    // Remove all layers in the group with the given ID
    $.fn.removeLayerGroup = function removeLayerGroup(groupId) {
      const $canvases = this
      let $canvas
      let e
      let group
      let l

      if (groupId !== undefined) {
        for (e = 0; e < $canvases.length; e += 1) {
          $canvas = $($canvases[e])

          group = $canvas.getLayerGroup(groupId)
          // Remove layer group using given group name
          if (group) {
            // Clone groups array
            group = group.slice(0)

            // Loop through layers in group
            for (l = 0; l < group.length; l += 1) {
              $canvas.removeLayer(group[l])
            }
          }
        }
      }
      return $canvases
    }

    // Add an existing layer to a layer group
    $.fn.addLayerToGroup = function addLayerToGroup(layerId, groupName) {
      const $canvases = this
      let $canvas
      let e
      let layer
      let groups = [groupName]

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        layer = $canvas.getLayer(layerId)

        // If layer is not already in group
        if (layer.groups) {
          // Clone groups list
          groups = layer.groups.slice(0)
          // If layer is not already in group
          if (inArray(groupName, layer.groups) === -1) {
            // Add layer to group
            groups.push(groupName)
          }
        }
        // Update layer group maps
        $canvas.setLayer(layer, {
          groups
        })
      }
      return $canvases
    }

    // Remove an existing layer from a layer group
    $.fn.removeLayerFromGroup = function removeLayerFromGroup(layerId, groupName) {
      const $canvases = this
      let $canvas
      let e
      let layer
      let groups = []
      let index

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        layer = $canvas.getLayer(layerId)

        if (layer.groups) {
          // Find index of layer in group
          index = inArray(groupName, layer.groups)

          // If layer is in group
          if (index !== -1) {
            // Clone groups list
            groups = layer.groups.slice(0)

            // Remove layer from group
            groups.splice(index, 1)

            // Update layer group maps
            $canvas.setLayer(layer, {
              groups
            })
          }
        }
      }
      return $canvases
    }

    // Get topmost layer that intersects with event coordinates
    function _getIntersectingLayer(data) {
      let layer
      let i
      let mask
      let m

      // Store the topmost layer
      layer = null

      // Get the topmost layer whose visible area intersects event coordinates
      for (i = data.intersecting.length - 1; i >= 0; i -= 1) {
        // Get current layer
        layer = data.intersecting[i]

        // If layer has previous masks
        if (layer._masks) {
          // Search previous masks to ensure
          // layer is visible at event coordinates
          for (m = layer._masks.length - 1; m >= 0; m -= 1) {
            mask = layer._masks[m]
            // If mask does not intersect event coordinates
            if (!mask.intersects) {
              // Indicate that the mask does not
              // intersect event coordinates
              layer.intersects = false
              // Stop searching previous masks
              break
            }
          }

          // If event coordinates intersect all previous masks
          // and layer is not intangible
          if (layer.intersects && !layer.intangible) {
            // Stop searching for topmost layer
            break
          }
        }
      }
      // If resulting layer is intangible
      if (layer && layer.intangible) {
        // Cursor does not intersect this layer
        layer = null
      }
      return layer
    }

    // Draw individual layer (internal)
    function _drawLayer($canvas, ctx, layer, nextLayerIndex) {
      if (layer && layer.visible && layer._method) {
        if (nextLayerIndex) {
          layer._next = nextLayerIndex
        } else {
          layer._next = null
        }
        // If layer is an object, call its respective method
        if (layer._method) {
          layer._method.call($canvas, layer)
        }
      }
    }

    // Handle dragging of the currently-dragged layer
    function _handleLayerDrag($canvas, data, eventType) {
      let layers
      let layer
      let l
      let drag
      let dragGroups
      let group
      let groupName
      let g
      let newX
      let newY

      drag = data.drag
      layer = drag.layer
      dragGroups = (layer && layer.dragGroups) || []
      layers = data.layers

      if (eventType === 'mousemove' || eventType === 'touchmove') {
        // Detect when user is currently dragging layer

        if (!drag.dragging) {
          // Detect when user starts dragging layer

          // Signify that a layer on the canvas is being dragged
          drag.dragging = true
          layer.dragging = true

          // Optionally bring layer to front when drag starts
          if (layer.bringToFront) {
            // Remove layer from its original position
            layers.splice(layer.index, 1)
            // Bring layer to front
            // push() returns the new array length
            layer.index = layers.push(layer)
          }

          // Set drag properties for this layer
          layer._startX = layer.x
          layer._startY = layer.y
          layer._endX = layer._eventX
          layer._endY = layer._eventY

          // Trigger dragstart event
          _triggerLayerEvent($canvas, data, layer, 'dragstart')
        }

        if (drag.dragging) {
          // Calculate position after drag
          newX = layer._eventX - (layer._endX - layer._startX)
          newY = layer._eventY - (layer._endY - layer._startY)
          if (layer.updateDragX) {
            newX = layer.updateDragX.call($canvas[0], layer, newX)
          }
          if (layer.updateDragY) {
            newY = layer.updateDragY.call($canvas[0], layer, newY)
          }
          layer.dx = newX - layer.x
          layer.dy = newY - layer.y
          if (layer.restrictDragToAxis !== 'y') {
            layer.x = newX
          }
          if (layer.restrictDragToAxis !== 'x') {
            layer.y = newY
          }

          // Trigger drag event
          _triggerLayerEvent($canvas, data, layer, 'drag')

          // Move groups with layer on drag
          for (g = 0; g < dragGroups.length; g += 1) {
            groupName = dragGroups[g]
            group = data.layer.groups[groupName]
            if (layer.groups && group) {
              for (l = 0; l < group.length; l += 1) {
                if (group[l] !== layer) {
                  if (layer.restrictDragToAxis !== 'y' && group[l].restrictDragToAxis !== 'y') {
                    group[l].x += layer.dx
                  }
                  if (layer.restrictDragToAxis !== 'x' && group[l].restrictDragToAxis !== 'x') {
                    group[l].y += layer.dy
                  }
                }
              }
            }
          }
        }
      } else if (eventType === 'mouseup' || eventType === 'touchend') {
        // Detect when user stops dragging layer

        if (drag.dragging) {
          layer.dragging = false
          drag.dragging = false
          data.redrawOnMousemove = data.originalRedrawOnMousemove
          // Trigger dragstop event
          _triggerLayerEvent($canvas, data, layer, 'dragstop')
        }

        // Cancel dragging
        data.drag = {}
      }
    }

    // List of CSS3 cursors that need to be prefixed
    css.cursors = ['grab', 'grabbing', 'zoom-in', 'zoom-out']

    // Function to detect vendor prefix
    // Modified version of David Walsh's implementation
    // https://davidwalsh.name/vendor-prefix
    css.prefix = (function() {
      const styles = getComputedStyle(document.documentElement, '')
      const pre = (arraySlice
        .call(styles)
        .join('')
        .match(/-(moz|webkit|ms)-/) ||
        (styles.OLink === '' && ['', 'o']))[1]
      return `-${pre}-`
    })()

    // Set cursor on canvas
    function _setCursor($canvas, layer, eventType) {
      let cursor
      if (layer.cursors) {
        // Retrieve cursor from cursors object if it exists
        cursor = layer.cursors[eventType]
      }
      // Prefix any CSS3 cursor
      if ($.inArray(cursor, css.cursors) !== -1) {
        cursor = css.prefix + cursor
      }
      // If cursor is defined
      if (cursor) {
        // Set canvas cursor
        $canvas.css({
          cursor
        })
      }
    }

    // Reset cursor on canvas
    function _resetCursor($canvas, data) {
      $canvas.css({
        cursor: data.cursor
      })
    }

    // Run the given event callback with the given arguments
    function _runEventCallback($canvas, layer, eventType, callbacks, arg) {
      // Prevent callback from firing recursively
      if (callbacks[eventType] && layer._running && !layer._running[eventType]) {
        // Signify the start of callback execution for this event
        layer._running[eventType] = true
        // Run event callback with the given arguments
        callbacks[eventType].call($canvas[0], layer, arg)
        // Signify the end of callback execution for this event
        layer._running[eventType] = false
      }
    }

    // Determine if the given layer can "legally" fire the given event
    function _layerCanFireEvent(layer, eventType) {
      // If events are disable and if
      // layer is tangible or event is not tangible
      return (
        !layer.disableEvents && (!layer.intangible || $.inArray(eventType, tangibleEvents) === -1)
      )
    }

    // Trigger the given event on the given layer
    function _triggerLayerEvent($canvas, data, layer, eventType, arg) {
      // If layer can legally fire this event type
      if (_layerCanFireEvent(layer, eventType)) {
        // Do not set a custom cursor on layer mouseout
        if (eventType !== 'mouseout') {
          // Update cursor if one is defined for this event
          _setCursor($canvas, layer, eventType)
        }

        // Trigger the user-defined event callback
        _runEventCallback($canvas, layer, eventType, layer, arg)
        // Trigger the canvas-bound event hook
        _runEventCallback($canvas, layer, eventType, data.eventHooks, arg)
        // Trigger the global event hook
        _runEventCallback($canvas, layer, eventType, jCanvas.eventHooks, arg)
      }
    }

    // Manually trigger a layer event
    $.fn.triggerLayerEvent = function(layer, eventType) {
      const $canvases = this
      let $canvas
      let e
      let data

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        data = _getCanvasData($canvases[e])
        layer = $canvas.getLayer(layer)
        if (layer) {
          _triggerLayerEvent($canvas, data, layer, eventType)
        }
      }
      return $canvases
    }

    // Draw layer with the given ID
    $.fn.drawLayer = function drawLayer(layerId) {
      const $canvases = this
      let e
      let ctx
      let $canvas
      let layer

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        ctx = _getContext($canvases[e])
        if (ctx) {
          layer = $canvas.getLayer(layerId)
          _drawLayer($canvas, ctx, layer)
        }
      }
      return $canvases
    }

    // Draw all layers (or, if given, only layers starting at an index)
    $.fn.drawLayers = function drawLayers(args) {
      const $canvases = this
      let $canvas
      let e
      let ctx
      // Internal parameters for redrawing the canvas
      const params = args || {}
      // Other variables
      let layers
      let layer
      let lastLayer
      let l
      let index
      let lastIndex
      let data
      let eventCache
      let eventType
      let isImageLayer

      // The layer index from which to start redrawing the canvas
      index = params.index
      if (!index) {
        index = 0
      }

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        ctx = _getContext($canvases[e])
        if (ctx) {
          data = _getCanvasData($canvases[e])

          // Clear canvas first unless otherwise directed
          if (params.clear !== false) {
            $canvas.clearCanvas()
          }

          // If a completion callback was provided, save it to the canvas data
          // store so that the function can be passed to drawLayers() again
          // after any image layers have loaded
          if (params.complete) {
            data.drawLayersComplete = params.complete
          }

          // Cache the layers array
          layers = data.layers

          // Draw layers from first to last (bottom to top)
          for (l = index; l < layers.length; l += 1) {
            layer = layers[l]

            // Ensure layer index is up-to-date
            layer.index = l

            // Prevent any one event from firing excessively
            if (params.resetFire) {
              layer._fired = false
            }
            // Draw layer
            _drawLayer($canvas, ctx, layer, l + 1)
            // Store list of previous masks for each layer
            layer._masks = data.transforms.masks.slice(0)

            // Allow image layers to load before drawing successive layers
            if (layer._method === $.fn.drawImage && layer.visible) {
              isImageLayer = true
              break
            }
          }

          // If layer is an image layer
          if (isImageLayer) {
            // Stop and wait for drawImage() to resume drawLayers()
            // eslint-disable-next-line no-continue
            continue
          }

          // Store the latest
          lastIndex = l

          // Run completion callback (if provided) once all layers have drawn
          if (params.complete) {
            params.complete.call($canvases[e])
            delete data.drawLayersComplete
          }

          // Get first layer that intersects with event coordinates
          layer = _getIntersectingLayer(data)

          eventCache = data.event
          eventType = eventCache.type

          // If jCanvas has detected a dragstart
          if (data.drag.layer) {
            // Handle dragging of layer
            _handleLayerDrag($canvas, data, eventType)
          }

          // Manage mouseout event
          lastLayer = data.lastIntersected
          if (
            lastLayer !== null &&
            layer !== lastLayer &&
            lastLayer._hovered &&
            !lastLayer._fired &&
            !data.drag.dragging
          ) {
            data.lastIntersected = null
            lastLayer._fired = true
            lastLayer._hovered = false
            _triggerLayerEvent($canvas, data, lastLayer, 'mouseout')
            _resetCursor($canvas, data)
          }

          if (layer) {
            // Use mouse event callbacks if no touch event callbacks are given
            if (!layer[eventType]) {
              eventType = _getMouseEventName(eventType)
            }

            // Check events for intersecting layer
            if (layer._event && layer.intersects) {
              data.lastIntersected = layer

              // Detect mouseover events
              if ((layer.mouseover || layer.mouseout || layer.cursors) && !data.drag.dragging) {
                if (!layer._hovered && !layer._fired) {
                  // Prevent events from firing excessively
                  layer._fired = true
                  layer._hovered = true
                  _triggerLayerEvent($canvas, data, layer, 'mouseover')
                }
              }

              // Detect any other mouse event
              if (!layer._fired) {
                // Prevent event from firing twice unintentionally
                layer._fired = true
                eventCache.type = null

                _triggerLayerEvent($canvas, data, layer, eventType)
              }

              // Use the mousedown event to start drag
              if (
                layer.draggable &&
                !layer.disableEvents &&
                (eventType === 'mousedown' || eventType === 'touchstart')
              ) {
                // Keep track of drag state
                data.drag.layer = layer
                data.originalRedrawOnMousemove = data.redrawOnMousemove
                data.redrawOnMousemove = true
              }
            }
          }

          // If cursor is not intersecting with any layer
          if (layer === null && !data.drag.dragging) {
            // Reset cursor to previous state
            _resetCursor($canvas, data)
          }

          // If the last layer has been drawn
          if (lastIndex === layers.length) {
            // Reset list of intersecting layers
            data.intersecting.length = 0
            // Reset transformation stack
            data.transforms = _cloneTransforms(baseTransforms)
            data.savedTransforms.length = 0
          }
        }
      }
      return $canvases
    }

    // Add a jCanvas layer (internal)
    function _addLayer(canvas, params, args, method) {
      let $canvas
      let data
      let layers
      let layer = params._layer ? args : params

      // Store arguments object for later use
      params._args = args

      // Convert all draggable drawings into jCanvas layers
      if (params.draggable || params.dragGroups) {
        params.layer = true
        params.draggable = true
      }

      // Determine the layer's type using the available information
      if (!params._method) {
        if (method) {
          params._method = method
        } else if (params.method) {
          params._method = $.fn[params.method]
        } else if (params.type) {
          params._method = $.fn[maps.drawings[params.type]]
        }
      }

      // If layer hasn't been added yet
      if (params.layer && !params._layer) {
        // Add layer to canvas

        $canvas = $(canvas)

        data = _getCanvasData(canvas)
        layers = data.layers

        // Do not add duplicate layers of same name
        if (
          layer.name === null ||
          (isString(layer.name) && data.layer.names[layer.name] === undefined)
        ) {
          // Convert number properties to numbers
          _coerceNumericProps(params)

          // Ensure layers are unique across canvases by cloning them
          layer = new jCanvasObject(params)
          layer.canvas = canvas
          // Indicate that this is a layer for future checks
          layer.layer = true
          layer._layer = true
          layer._running = {}
          // If layer stores user-defined data
          if (layer.data !== null) {
            // Clone object
            layer.data = extendObject({}, layer.data)
          } else {
            // Otherwise, create data object
            layer.data = {}
          }
          // If layer stores a list of associated groups
          if (layer.groups !== null) {
            // Clone list
            layer.groups = layer.groups.slice(0)
          } else {
            // Otherwise, create empty list
            layer.groups = []
          }

          // Update layer group maps
          _updateLayerName($canvas, data, layer)
          _updateLayerGroups($canvas, data, layer)

          // Check for any associated jCanvas events and enable them
          _addLayerEvents($canvas, data, layer)

          // Optionally enable drag-and-drop support and cursor support
          _enableDrag($canvas, data, layer)

          // Copy _event property to parameters object
          params._event = layer._event

          // Calculate width/height for text layers
          if (layer._method === $.fn.drawText) {
            $canvas.measureText(layer)
          }

          // Add layer to end of array if no index is specified
          if (layer.index === null) {
            layer.index = layers.length
          }

          // Add layer to layers array at specified index
          layers.splice(layer.index, 0, layer)

          // Store layer on parameters object
          params._args = layer

          // Trigger an 'add' event
          _triggerLayerEvent($canvas, data, layer, 'add')
        }
      } else if (!params.layer) {
        _coerceNumericProps(params)
      }

      return layer
    }

    // Add a jCanvas layer
    $.fn.addLayer = function addLayer(args) {
      const $canvases = this
      let e
      let ctx
      let params
      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          params.layer = true
          _addLayer($canvases[e], params, args)
        }
      }
      return $canvases
    }

    /* Animation API */

    // Define properties used in both CSS and jCanvas
    css.props = ['width', 'height', 'opacity', 'lineHeight']
    css.propsObj = {}

    // Hide/show jCanvas/CSS properties so they can be animated using jQuery
    function _showProps(obj) {
      let cssProp
      let p
      for (p = 0; p < css.props.length; p += 1) {
        cssProp = css.props[p]
        obj[cssProp] = obj[`_${cssProp}`]
      }
    }
    function _hideProps(obj, reset) {
      let cssProp
      let p
      for (p = 0; p < css.props.length; p += 1) {
        cssProp = css.props[p]
        // Hide property using same name with leading underscore
        if (obj[cssProp] !== undefined) {
          obj[`_${cssProp}`] = obj[cssProp]
          css.propsObj[cssProp] = true
          if (reset) {
            delete obj[cssProp]
          }
        }
      }
    }

    // Evaluate property values that are functions
    function _parseEndValues(canvas, layer, endValues) {
      let propName
      let propValue
      let subPropName
      let subPropValue
      // Loop through all properties in map of end values
      for (propName in endValues) {
        if (Object.prototype.hasOwnProperty.call(endValues, propName)) {
          propValue = endValues[propName]
          // If end value is function
          if (isFunction(propValue)) {
            // Call function and use its value as the end value
            endValues[propName] = propValue.call(canvas, layer, propName)
          }
          // If end value is an object
          if (typeOf(propValue) === 'object' && isPlainObject(propValue)) {
            // Prepare to animate properties in object
            for (subPropName in propValue) {
              if (Object.prototype.hasOwnProperty.call(propValue, subPropName)) {
                subPropValue = propValue[subPropName]
                // Store property's start value at top-level of layer
                if (layer[propName] !== undefined) {
                  layer[`${propName}.${subPropName}`] = layer[propName][subPropName]
                  // Store property's end value at top-level of end values map
                  endValues[`${propName}.${subPropName}`] = subPropValue
                }
              }
            }
            // Delete sub-property of object as it's no longer needed
            delete endValues[propName]
          }
        }
      }
      return endValues
    }

    // Remove sub-property aliases from layer object
    function _removeSubPropAliases(layer) {
      let propName
      for (propName in layer) {
        if (Object.prototype.hasOwnProperty.call(layer, propName)) {
          if (propName.indexOf('.') !== -1) {
            delete layer[propName]
          }
        }
      }
    }

    // Convert a color value to an array of RGB values
    function _colorToRgbArray(color) {
      let originalColor
      let elem
      let rgb = []
      let multiple = 1

      // Deal with complete transparency
      if (color === 'transparent') {
        color = 'rgba(0, 0, 0, 0)'
      } else if (color.match(/^([a-z]+|#[0-9a-f]+)$/gi)) {
        // Deal with hexadecimal colors and color names
        elem = document.head
        originalColor = elem.style.color
        elem.style.color = color
        color = $.css(elem, 'color')
        elem.style.color = originalColor
      }
      // Parse RGB string
      if (color.match(/^rgb/gi)) {
        rgb = color.match(/(\d+(\.\d+)?)/gi)
        // Deal with RGB percentages
        if (color.match(/%/gi)) {
          multiple = 2.55
        }
        rgb[0] *= multiple
        rgb[1] *= multiple
        rgb[2] *= multiple
        // Ad alpha channel if given
        if (rgb[3] !== undefined) {
          rgb[3] = parseFloat(rgb[3])
        } else {
          rgb[3] = 1
        }
      }
      return rgb
    }

    // Animate a hex or RGB color
    function _animateColor(fx) {
      let n = 3
      let i
      // Only parse start and end colors once
      if (typeOf(fx.start) !== 'array') {
        fx.start = _colorToRgbArray(fx.start)
        fx.end = _colorToRgbArray(fx.end)
      }
      fx.now = []

      // If colors are RGBA, animate transparency
      if (fx.start[3] !== 1 || fx.end[3] !== 1) {
        n = 4
      }

      // Calculate current frame for red, green, blue, and alpha
      for (i = 0; i < n; i += 1) {
        fx.now[i] = fx.start[i] + (fx.end[i] - fx.start[i]) * fx.pos
        // Only the red, green, and blue values must be integers
        if (i < 3) {
          fx.now[i] = round(fx.now[i])
        }
      }
      if (fx.start[3] !== 1 || fx.end[3] !== 1) {
        // Only use RGBA if RGBA colors are given
        fx.now = `rgba(${fx.now.join(',')})`
      } else {
        // Otherwise, animate as solid colors
        fx.now.slice(0, 3)
        fx.now = `rgb(${fx.now.join(',')})`
      }
      // Animate colors for both canvas layers and DOM elements
      if (fx.elem.nodeName) {
        fx.elem.style[fx.prop] = fx.now
      } else {
        fx.elem[fx.prop] = fx.now
      }
    }

    // Animate jCanvas layer
    $.fn.animateLayer = function animateLayer() {
      const $canvases = this
      let $canvas
      let e
      let ctx
      // eslint-disable-next-line prefer-rest-params
      const args = arraySlice.call(arguments, 0)
      let data
      let layer
      let props

      // Deal with all cases of argument placement
      /*
		0. layer name/index
		1. properties
		2. duration/options
		3. easing
		4. complete function
		5. step function
	*/

      if (typeOf(args[2]) === 'object') {
        // Accept an options object for animation
        args.splice(2, 0, args[2].duration || null)
        args.splice(3, 0, args[3].easing || null)
        args.splice(4, 0, args[4].complete || null)
        args.splice(5, 0, args[5].step || null)
      } else {
        if (args[2] === undefined) {
          // If object is the last argument
          args.splice(2, 0, null)
          args.splice(3, 0, null)
          args.splice(4, 0, null)
        } else if (isFunction(args[2])) {
          // If callback comes after object
          args.splice(2, 0, null)
          args.splice(3, 0, null)
        }
        if (args[3] === undefined) {
          // If duration is the last argument
          args[3] = null
          args.splice(4, 0, null)
        } else if (isFunction(args[3])) {
          // If callback comes after duration
          args.splice(3, 0, null)
        }
      }

      // Run callback function when animation completes
      function complete($canvas, data, layer) {
        return function() {
          _showProps(layer)
          _removeSubPropAliases(layer)

          // Prevent multiple redraw loops
          if (!data.animating || data.animated === layer) {
            // Redraw layers on last frame
            $canvas.drawLayers()
          }

          // Signify the end of an animation loop
          layer._animating = false
          data.animating = false
          data.animated = null

          // If callback is defined
          if (args[4]) {
            // Run callback at the end of the animation
            args[4].call($canvas[0], layer)
          }

          _triggerLayerEvent($canvas, data, layer, 'animateend')
        }
      }

      // Redraw layers on every frame of the animation
      function step($canvas, data, layer) {
        return function(now, fx) {
          let parts
          let propName
          let subPropName
          let hidden = false

          // If animated property has been hidden
          if (fx.prop[0] === '_') {
            hidden = true
            // Unhide property temporarily
            fx.prop = fx.prop.replace('_', '')
            layer[fx.prop] = layer[`_${fx.prop}`]
          }

          // If animating property of sub-object
          if (fx.prop.indexOf('.') !== -1) {
            parts = fx.prop.split('.')
            propName = parts[0]
            subPropName = parts[1]
            if (layer[propName]) {
              layer[propName][subPropName] = fx.now
            }
          }

          // Throttle animation to improve efficiency
          if (layer._pos !== fx.pos) {
            layer._pos = fx.pos

            // Signify the start of an animation loop
            if (!layer._animating && !data.animating) {
              layer._animating = true
              data.animating = true
              data.animated = layer
            }

            // Prevent multiple redraw loops
            if (!data.animating || data.animated === layer) {
              // Redraw layers for every frame
              $canvas.drawLayers()
            }
          }

          // If callback is defined
          if (args[5]) {
            // Run callback for each step of animation
            args[5].call($canvas[0], now, fx, layer)
          }

          _triggerLayerEvent($canvas, data, layer, 'animate', fx)

          // If property should be hidden during animation
          if (hidden) {
            // Hide property again
            fx.prop = `_${fx.prop}`
          }
        }
      }

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        ctx = _getContext($canvases[e])
        if (ctx) {
          data = _getCanvasData($canvases[e])

          // If a layer object was passed, use it the layer to be animated
          layer = $canvas.getLayer(args[0])

          // Ignore layers that are functions
          if (layer && layer._method !== $.fn.draw) {
            // Do not modify original object
            props = extendObject({}, args[1])

            props = _parseEndValues($canvases[e], layer, props)

            // Bypass jQuery CSS Hooks for CSS properties (width, opacity, etc.)
            _hideProps(props, true)
            _hideProps(layer)

            // Fix for jQuery's vendor prefixing support, which affects how width/height/opacity are animated
            layer.style = css.propsObj

            // Animate layer
            $(layer).animate(props, {
              duration: args[2],
              easing: $.easing[args[3]] ? args[3] : null,
              // When animation completes
              complete: complete($canvas, data, layer),
              // Redraw canvas for every animation frame
              step: step($canvas, data, layer)
            })
            _triggerLayerEvent($canvas, data, layer, 'animatestart')
          }
        }
      }
      return $canvases
    }

    // Animate all layers in a layer group
    $.fn.animateLayerGroup = function animateLayerGroup(groupId) {
      const $canvases = this
      let $canvas
      let e
      // eslint-disable-next-line prefer-rest-params
      const args = arraySlice.call(arguments, 0)
      let group
      let l
      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        group = $canvas.getLayerGroup(groupId)
        if (group) {
          // Animate all layers in the group
          for (l = 0; l < group.length; l += 1) {
            // Replace first argument with layer
            args[0] = group[l]
            $canvas.animateLayer.apply($canvas, args)
          }
        }
      }
      return $canvases
    }

    // Delay layer animation by a given number of milliseconds
    $.fn.delayLayer = function delayLayer(layerId, duration) {
      const $canvases = this
      let $canvas
      let e
      let data
      let layer
      duration = duration || 0

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        data = _getCanvasData($canvases[e])
        layer = $canvas.getLayer(layerId)
        // If layer exists
        if (layer) {
          // Delay animation
          $(layer).delay(duration)
          _triggerLayerEvent($canvas, data, layer, 'delay')
        }
      }
      return $canvases
    }

    // Delay animation all layers in a layer group
    $.fn.delayLayerGroup = function delayLayerGroup(groupId, duration) {
      const $canvases = this
      let $canvas
      let e
      let group
      let layer
      let l
      duration = duration || 0

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])

        group = $canvas.getLayerGroup(groupId)
        // Delay all layers in the group
        if (group) {
          for (l = 0; l < group.length; l += 1) {
            // Delay each layer in the group
            layer = group[l]
            $canvas.delayLayer(layer, duration)
          }
        }
      }
      return $canvases
    }

    // Stop layer animation
    $.fn.stopLayer = function stopLayer(layerId, clearQueue) {
      const $canvases = this
      let $canvas
      let e
      let data
      let layer

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])
        data = _getCanvasData($canvases[e])
        layer = $canvas.getLayer(layerId)
        // If layer exists
        if (layer) {
          // Stop animation
          $(layer).stop(clearQueue)
          _triggerLayerEvent($canvas, data, layer, 'stop')
        }
      }
      return $canvases
    }

    // Stop animation of all layers in a layer group
    $.fn.stopLayerGroup = function stopLayerGroup(groupId, clearQueue) {
      const $canvases = this
      let $canvas
      let e
      let group
      let layer
      let l

      for (e = 0; e < $canvases.length; e += 1) {
        $canvas = $($canvases[e])

        group = $canvas.getLayerGroup(groupId)
        // Stop all layers in the group
        if (group) {
          for (l = 0; l < group.length; l += 1) {
            // Stop each layer in the group
            layer = group[l]
            $canvas.stopLayer(layer, clearQueue)
          }
        }
      }
      return $canvases
    }

    // Enable animation for color properties
    function _supportColorProps(props) {
      let p
      for (p = 0; p < props.length; p += 1) {
        $.fx.step[props[p]] = _animateColor
      }
    }

    // Enable animation for color properties
    _supportColorProps([
      'color',
      'backgroundColor',
      'borderColor',
      'borderTopColor',
      'borderRightColor',
      'borderBottomColor',
      'borderLeftColor',
      'fillStyle',
      'outlineColor',
      'strokeStyle',
      'shadowColor'
    ])

    /* Event API */

    // Map standard mouse events to touch events
    maps.touchEvents = {
      mousedown: 'touchstart',
      mouseup: 'touchend',
      mousemove: 'touchmove'
    }
    // Map standard touch events to mouse events
    maps.mouseEvents = {
      touchstart: 'mousedown',
      touchend: 'mouseup',
      touchmove: 'mousemove'
    }

    // Convert mouse event name to a corresponding touch event name (if possible)
    function _getTouchEventName(eventName) {
      // Detect touch event support
      if (maps.touchEvents[eventName]) {
        eventName = maps.touchEvents[eventName]
      }
      return eventName
    }
    // Convert touch event name to a corresponding mouse event name
    function _getMouseEventName(eventName) {
      if (maps.mouseEvents[eventName]) {
        eventName = maps.mouseEvents[eventName]
      }
      return eventName
    }

    // Bind event to jCanvas layer using standard jQuery events
    function _createEvent(eventName) {
      jCanvas.events[eventName] = function($canvas, data) {
        let helperEventName
        let touchEventName
        let eventCache

        // Retrieve canvas's event cache
        eventCache = data.event

        // Both mouseover/mouseout events will be managed by a single mousemove event
        helperEventName =
          eventName === 'mouseover' || eventName === 'mouseout' ? 'mousemove' : eventName
        touchEventName = _getTouchEventName(helperEventName)

        function eventCallback(event) {
          // Cache current mouse position and redraw layers
          eventCache.x = event.offsetX
          eventCache.y = event.offsetY
          eventCache.type = helperEventName
          eventCache.event = event
          // Redraw layers on every trigger of the event; don't redraw if at
          // least one layer is draggable and there are no layers with
          // explicit mouseover/mouseout/mousemove events
          if (event.type !== 'mousemove' || data.redrawOnMousemove || data.drag.dragging) {
            $canvas.drawLayers({
              resetFire: true
            })
          }
          // Prevent default event behavior
          event.preventDefault()
        }

        // Ensure the event is not bound more than once
        if (!data.events[helperEventName]) {
          // Bind one canvas event which handles all layer events of that type
          if (touchEventName !== helperEventName) {
            $canvas.bind(`${helperEventName}.jCanvas ${touchEventName}.jCanvas`, eventCallback)
          } else {
            $canvas.bind(`${helperEventName}.jCanvas`, eventCallback)
          }
          // Prevent this event from being bound twice
          data.events[helperEventName] = true
        }
      }
    }
    function _createEvents(eventNames) {
      let n
      for (n = 0; n < eventNames.length; n += 1) {
        _createEvent(eventNames[n])
      }
    }
    // Populate jCanvas events object with some standard events
    _createEvents([
      'click',
      'dblclick',
      'mousedown',
      'mouseup',
      'mousemove',
      'mouseover',
      'mouseout',
      'touchstart',
      'touchmove',
      'touchend',
      'pointerdown',
      'pointermove',
      'pointerup',
      'contextmenu'
    ])

    // Check if event fires when a drawing is drawn
    function _detectEvents(canvas, ctx, params) {
      let layer
      let data
      let eventCache
      let intersects
      let transforms
      let x
      let y
      let angle

      // Use the layer object stored by the given parameters object
      layer = params._args
      // Canvas must have event bindings
      if (layer) {
        data = _getCanvasData(canvas)
        eventCache = data.event
        if (eventCache.x !== null && eventCache.y !== null) {
          // Respect user-defined pixel ratio
          x = eventCache.x * data.pixelRatio
          y = eventCache.y * data.pixelRatio
          // Determine if the given coordinates are in the current path
          intersects = ctx.isPointInPath(x, y) || (ctx.isPointInStroke && ctx.isPointInStroke(x, y))
        }
        transforms = data.transforms

        // Allow callback functions to retrieve the mouse coordinates
        layer.eventX = eventCache.x
        layer.eventY = eventCache.y
        layer.event = eventCache.event

        // Adjust coordinates to match current canvas transformation

        // Keep track of some transformation values
        angle = data.transforms.rotate
        x = layer.eventX
        y = layer.eventY

        if (angle !== 0) {
          // Rotate coordinates if coordinate space has been rotated
          layer._eventX = x * cos(-angle) - y * sin(-angle)
          layer._eventY = y * cos(-angle) + x * sin(-angle)
        } else {
          // Otherwise, no calculations need to be made
          layer._eventX = x
          layer._eventY = y
        }

        // Scale coordinates
        layer._eventX /= transforms.scaleX
        layer._eventY /= transforms.scaleY

        // If layer intersects with cursor
        if (intersects) {
          // Add it to a list of layers that intersect with cursor
          data.intersecting.push(layer)
        }
        layer.intersects = Boolean(intersects)
      }
    }

    // Normalize offsetX and offsetY for all browsers
    $.event.fix = function(event) {
      let offset
      let originalEvent
      let touches

      event = jQueryEventFix.call($.event, event)
      originalEvent = event.originalEvent

      // originalEvent does not exist for manually-triggered events
      if (originalEvent) {
        touches = originalEvent.changedTouches

        // If offsetX and offsetY are not supported, define them
        if (event.pageX !== undefined && event.offsetX === undefined) {
          try {
            offset = $(event.currentTarget).offset()
            if (offset) {
              event.offsetX = event.pageX - offset.left
              event.offsetY = event.pageY - offset.top
            }
          } catch (error) {
            // Fail silently
          }
        } else if (touches) {
          try {
            // Enable offsetX and offsetY for mobile devices
            offset = $(event.currentTarget).offset()
            if (offset) {
              event.offsetX = touches[0].pageX - offset.left
              event.offsetY = touches[0].pageY - offset.top
            }
          } catch (error) {
            // Fail silently
          }
        }
      }
      return event
    }

    /* Drawing API */

    // Map drawing names with their respective method names
    maps.drawings = {
      arc: 'drawArc',
      bezier: 'drawBezier',
      ellipse: 'drawEllipse',
      function: 'draw',
      image: 'drawImage',
      line: 'drawLine',
      path: 'drawPath',
      polygon: 'drawPolygon',
      slice: 'drawSlice',
      quadratic: 'drawQuadratic',
      rectangle: 'drawRect',
      text: 'drawText',
      vector: 'drawVector',
      save: 'saveCanvas',
      restore: 'restoreCanvas',
      rotate: 'rotateCanvas',
      scale: 'scaleCanvas',
      translate: 'translateCanvas'
    }

    // Draws on canvas using a function
    $.fn.draw = function draw(args) {
      const $canvases = this
      let e
      let ctx
      let params = new jCanvasObject(args)

      // Draw using any other method
      if (maps.drawings[params.type] && params.type !== 'function') {
        $canvases[maps.drawings[params.type]](args)
      } else {
        for (e = 0; e < $canvases.length; e += 1) {
          ctx = _getContext($canvases[e])
          if (ctx) {
            params = new jCanvasObject(args)
            _addLayer($canvases[e], params, args, draw)
            if (params.visible) {
              if (params.fn) {
                // Call the given user-defined function
                params.fn.call($canvases[e], ctx, params)
              }
            }
          }
        }
      }
      return $canvases
    }

    // Clears canvas
    $.fn.clearCanvas = function clearCanvas(args) {
      const $canvases = this
      let e
      let ctx
      const params = new jCanvasObject(args)

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          if (params.width === null || params.height === null) {
            // Clear entire canvas if width/height is not given

            // Reset current transformation temporarily to ensure that the entire canvas is cleared
            ctx.save()
            ctx.setTransform(1, 0, 0, 1, 0, 0)
            ctx.clearRect(0, 0, $canvases[e].width, $canvases[e].height)
            ctx.restore()
          } else {
            // Otherwise, clear the defined section of the canvas

            // Transform clear rectangle
            _addLayer($canvases[e], params, args, clearCanvas)
            _transformShape($canvases[e], ctx, params, params.width, params.height)
            ctx.clearRect(
              params.x - params.width / 2,
              params.y - params.height / 2,
              params.width,
              params.height
            )
            // Restore previous transformation
            _restoreTransform(ctx, params)
          }
        }
      }
      return $canvases
    }

    /* Transformation API */

    // Restores canvas
    $.fn.saveCanvas = function saveCanvas(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let data
      let i

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          data = _getCanvasData($canvases[e])

          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, saveCanvas)

          // Restore a number of times using the given count
          for (i = 0; i < params.count; i += 1) {
            _saveCanvas(ctx, data)
          }
        }
      }
      return $canvases
    }

    // Restores canvas
    $.fn.restoreCanvas = function restoreCanvas(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let data
      let i

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          data = _getCanvasData($canvases[e])

          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, restoreCanvas)

          // Restore a number of times using the given count
          for (i = 0; i < params.count; i += 1) {
            _restoreCanvas(ctx, data)
          }
        }
      }
      return $canvases
    }

    // Rotates canvas (internal)
    function _rotateCanvas(ctx, params, transforms) {
      // Get conversion factor for radians
      params._toRad = params.inDegrees ? PI / 180 : 1

      // Rotate canvas using shape as center of rotation
      ctx.translate(params.x, params.y)
      ctx.rotate(params.rotate * params._toRad)
      ctx.translate(-params.x, -params.y)

      // If transformation data was given
      if (transforms) {
        // Update transformation data
        transforms.rotate += params.rotate * params._toRad
      }
    }

    // Scales canvas (internal)
    function _scaleCanvas(ctx, params, transforms) {
      // Scale both the x- and y- axis using the 'scale' property
      if (params.scale !== 1) {
        params.scaleX = params.scaleY = params.scale
      }

      // Scale canvas using shape as center of rotation
      ctx.translate(params.x, params.y)
      ctx.scale(params.scaleX, params.scaleY)
      ctx.translate(-params.x, -params.y)

      // If transformation data was given
      if (transforms) {
        // Update transformation data
        transforms.scaleX *= params.scaleX
        transforms.scaleY *= params.scaleY
      }
    }

    // Translates canvas (internal)
    function _translateCanvas(ctx, params, transforms) {
      // Translate both the x- and y-axis using the 'translate' property
      if (params.translate) {
        params.translateX = params.translateY = params.translate
      }

      // Translate canvas
      ctx.translate(params.translateX, params.translateY)

      // If transformation data was given
      if (transforms) {
        // Update transformation data
        transforms.translateX += params.translateX
        transforms.translateY += params.translateY
      }
    }

    // Rotates canvas
    $.fn.rotateCanvas = function rotateCanvas(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let data

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          data = _getCanvasData($canvases[e])

          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, rotateCanvas)

          // Autosave transformation state by default
          if (params.autosave) {
            // Automatically save transformation state by default
            _saveCanvas(ctx, data)
          }
          _rotateCanvas(ctx, params, data.transforms)
        }
      }
      return $canvases
    }

    // Scales canvas
    $.fn.scaleCanvas = function scaleCanvas(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let data

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          data = _getCanvasData($canvases[e])

          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, scaleCanvas)

          // Autosave transformation state by default
          if (params.autosave) {
            // Automatically save transformation state by default
            _saveCanvas(ctx, data)
          }
          _scaleCanvas(ctx, params, data.transforms)
        }
      }
      return $canvases
    }

    // Translates canvas
    $.fn.translateCanvas = function translateCanvas(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let data

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          data = _getCanvasData($canvases[e])

          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, translateCanvas)

          // Autosave transformation state by default
          if (params.autosave) {
            // Automatically save transformation state by default
            _saveCanvas(ctx, data)
          }
          _translateCanvas(ctx, params, data.transforms)
        }
      }
      return $canvases
    }

    /* Shape API */

    // Draws rectangle
    $.fn.drawRect = function drawRect(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let x1
      let y1
      let x2
      let y2
      let r
      let temp

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawRect)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params, params.width, params.height)
            _setGlobalProps($canvases[e], ctx, params)

            ctx.beginPath()
            if (params.width && params.height) {
              x1 = params.x - params.width / 2
              y1 = params.y - params.height / 2
              r = abs(params.cornerRadius)
              // If corner radius is defined and is not zero
              if (r) {
                // Draw rectangle with rounded corners if cornerRadius is defined

                x2 = params.x + params.width / 2
                y2 = params.y + params.height / 2

                // Handle negative width
                if (params.width < 0) {
                  temp = x1
                  x1 = x2
                  x2 = temp
                }
                // Handle negative height
                if (params.height < 0) {
                  temp = y1
                  y1 = y2
                  y2 = temp
                }

                // Prevent over-rounded corners
                if (x2 - x1 - 2 * r < 0) {
                  r = (x2 - x1) / 2
                }
                if (y2 - y1 - 2 * r < 0) {
                  r = (y2 - y1) / 2
                }

                // Draw rectangle
                ctx.moveTo(x1 + r, y1)
                ctx.lineTo(x2 - r, y1)
                ctx.arc(x2 - r, y1 + r, r, (3 * PI) / 2, PI * 2, false)
                ctx.lineTo(x2, y2 - r)
                ctx.arc(x2 - r, y2 - r, r, 0, PI / 2, false)
                ctx.lineTo(x1 + r, y2)
                ctx.arc(x1 + r, y2 - r, r, PI / 2, PI, false)
                ctx.lineTo(x1, y1 + r)
                ctx.arc(x1 + r, y1 + r, r, PI, (3 * PI) / 2, false)
                // Always close path
                params.closed = true
              } else {
                // Otherwise, draw rectangle with square corners
                ctx.rect(x1, y1, params.width, params.height)
              }
            }
            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Close rectangle path
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    // Retrieves a coterminal angle between 0 and 2pi for the given angle
    function _getCoterminal(angle) {
      while (angle < 0) {
        angle += 2 * PI
      }
      return angle
    }

    // Retrieves the x-coordinate for the given angle in a circle
    function _getArcX(params, angle) {
      return params.x + params.radius * cos(angle)
    }
    // Retrieves the y-coordinate for the given angle in a circle
    function _getArcY(params, angle) {
      return params.y + params.radius * sin(angle)
    }

    // Draws arc (internal)
    function _drawArc(canvas, ctx, params, path) {
      let x1
      let y1
      let x2
      let y2
      let x3
      let y3
      let x4
      let y4
      let offsetX
      let offsetY
      let diff

      // Determine offset from dragging
      if (params === path) {
        offsetX = 0
        offsetY = 0
      } else {
        offsetX = params.x
        offsetY = params.y
      }

      // Convert default end angle to radians
      if (!path.inDegrees && path.end === 360) {
        path.end = PI * 2
      }

      // Convert angles to radians
      path.start *= params._toRad
      path.end *= params._toRad
      // Consider 0deg due north of arc
      path.start -= PI / 2
      path.end -= PI / 2

      // Ensure arrows are pointed correctly for CCW arcs
      diff = PI / 180
      if (path.ccw) {
        diff *= -1
      }

      // Calculate coordinates for start arrow
      x1 = _getArcX(path, path.start + diff)
      y1 = _getArcY(path, path.start + diff)
      x2 = _getArcX(path, path.start)
      y2 = _getArcY(path, path.start)

      _addStartArrow(canvas, ctx, params, path, x1, y1, x2, y2)

      // Draw arc
      ctx.arc(path.x + offsetX, path.y + offsetY, path.radius, path.start, path.end, path.ccw)

      // Calculate coordinates for end arrow
      x3 = _getArcX(path, path.end + diff)
      y3 = _getArcY(path, path.end + diff)
      x4 = _getArcX(path, path.end)
      y4 = _getArcY(path, path.end)

      _addEndArrow(canvas, ctx, params, path, x4, y4, x3, y3)
    }

    // Draws arc or circle
    $.fn.drawArc = function drawArc(args) {
      const $canvases = this
      let e
      let ctx
      let params

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawArc)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params, params.radius * 2)
            _setGlobalProps($canvases[e], ctx, params)

            ctx.beginPath()
            _drawArc($canvases[e], ctx, params, params)
            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Optionally close path
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    // Draws ellipse
    $.fn.drawEllipse = function drawEllipse(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let controlW
      let controlH

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawEllipse)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params, params.width, params.height)
            _setGlobalProps($canvases[e], ctx, params)

            // Calculate control width and height
            controlW = params.width * (4 / 3)
            controlH = params.height

            // Create ellipse using curves
            ctx.beginPath()
            ctx.moveTo(params.x, params.y - controlH / 2)
            // Left side
            ctx.bezierCurveTo(
              params.x - controlW / 2,
              params.y - controlH / 2,
              params.x - controlW / 2,
              params.y + controlH / 2,
              params.x,
              params.y + controlH / 2
            )
            // Right side
            ctx.bezierCurveTo(
              params.x + controlW / 2,
              params.y + controlH / 2,
              params.x + controlW / 2,
              params.y - controlH / 2,
              params.x,
              params.y - controlH / 2
            )
            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Always close path
            params.closed = true
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    // Draws a regular (equal-angled) polygon
    $.fn.drawPolygon = function drawPolygon(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let theta
      let dtheta
      let hdtheta
      let apothem
      let x
      let y
      let i

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawPolygon)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params, params.radius * 2)
            _setGlobalProps($canvases[e], ctx, params)

            // Polygon's central angle
            dtheta = (2 * PI) / params.sides
            // Half of dtheta
            hdtheta = dtheta / 2
            // Polygon's starting angle
            theta = hdtheta + PI / 2
            // Distance from polygon's center to the middle of its side
            apothem = params.radius * cos(hdtheta)

            // Calculate path and draw
            ctx.beginPath()
            for (i = 0; i < params.sides; i += 1) {
              // Draw side of polygon
              x = params.x + params.radius * cos(theta)
              y = params.y + params.radius * sin(theta)

              // Plot point on polygon
              ctx.lineTo(x, y)

              // Project side if chosen
              if (params.concavity) {
                // Sides are projected from the polygon's apothem
                x = params.x + (apothem + -apothem * params.concavity) * cos(theta + hdtheta)
                y = params.y + (apothem + -apothem * params.concavity) * sin(theta + hdtheta)
                ctx.lineTo(x, y)
              }

              // Increment theta by delta theta
              theta += dtheta
            }
            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Always close path
            params.closed = true
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    // Draws pie-shaped slice
    $.fn.drawSlice = function drawSlice(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let angle
      let dx
      let dy

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawSlice)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params, params.radius * 2)
            _setGlobalProps($canvases[e], ctx, params)

            // Perform extra calculations

            // Convert angles to radians
            params.start *= params._toRad
            params.end *= params._toRad
            // Consider 0deg at north of arc
            params.start -= PI / 2
            params.end -= PI / 2

            // Find positive equivalents of angles
            params.start = _getCoterminal(params.start)
            params.end = _getCoterminal(params.end)
            // Ensure start angle is less than end angle
            if (params.end < params.start) {
              params.end += 2 * PI
            }

            // Calculate angular position of slice
            angle = (params.start + params.end) / 2

            // Calculate ratios for slice's angle
            dx = params.radius * params.spread * cos(angle)
            dy = params.radius * params.spread * sin(angle)

            // Adjust position of slice
            params.x += dx
            params.y += dy

            // Draw slice
            ctx.beginPath()
            ctx.arc(params.x, params.y, params.radius, params.start, params.end, params.ccw)
            ctx.lineTo(params.x, params.y)
            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Always close path
            params.closed = true
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    /* Path API */

    // Adds arrow to path using the given properties
    function _addArrow(canvas, ctx, params, path, x1, y1, x2, y2) {
      let leftX
      let leftY
      let rightX
      let rightY
      let offsetX
      let offsetY
      let angle

      // If arrow radius is given and path is not closed
      if (path.arrowRadius && !params.closed) {
        // Calculate angle
        angle = atan2(y2 - y1, x2 - x1)
        // Adjust angle correctly
        angle -= PI
        // Calculate offset to place arrow at edge of path
        offsetX = params.strokeWidth * cos(angle)
        offsetY = params.strokeWidth * sin(angle)

        // Calculate coordinates for left half of arrow
        leftX = x2 + path.arrowRadius * cos(angle + path.arrowAngle / 2)
        leftY = y2 + path.arrowRadius * sin(angle + path.arrowAngle / 2)
        // Calculate coordinates for right half of arrow
        rightX = x2 + path.arrowRadius * cos(angle - path.arrowAngle / 2)
        rightY = y2 + path.arrowRadius * sin(angle - path.arrowAngle / 2)

        // Draw left half of arrow
        ctx.moveTo(leftX - offsetX, leftY - offsetY)
        ctx.lineTo(x2 - offsetX, y2 - offsetY)
        // Draw right half of arrow
        ctx.lineTo(rightX - offsetX, rightY - offsetY)

        // Visually connect arrow to path
        ctx.moveTo(x2 - offsetX, y2 - offsetY)
        ctx.lineTo(x2 + offsetX, y2 + offsetY)
        // Move back to end of path
        ctx.moveTo(x2, y2)
      }
    }

    // Optionally adds arrow to start of path
    function _addStartArrow(canvas, ctx, params, path, x1, y1, x2, y2) {
      if (!path._arrowAngleConverted) {
        path.arrowAngle *= params._toRad
        path._arrowAngleConverted = true
      }
      if (path.startArrow) {
        _addArrow(canvas, ctx, params, path, x1, y1, x2, y2)
      }
    }

    // Optionally adds arrow to end of path
    function _addEndArrow(canvas, ctx, params, path, x1, y1, x2, y2) {
      if (!path._arrowAngleConverted) {
        path.arrowAngle *= params._toRad
        path._arrowAngleConverted = true
      }
      if (path.endArrow) {
        _addArrow(canvas, ctx, params, path, x1, y1, x2, y2)
      }
    }

    // Draws line (internal)
    function _drawLine(canvas, ctx, params, path) {
      let l
      let lx
      let ly
      l = 2
      _addStartArrow(
        canvas,
        ctx,
        params,
        path,
        path.x2 + params.x,
        path.y2 + params.y,
        path.x1 + params.x,
        path.y1 + params.y
      )
      if (path.x1 !== undefined && path.y1 !== undefined) {
        ctx.moveTo(path.x1 + params.x, path.y1 + params.y)
      }
      while (true) {
        // Calculate next coordinates
        lx = path[`x${l}`]
        ly = path[`y${l}`]
        // If coordinates are given
        if (lx !== undefined && ly !== undefined) {
          // Draw next line
          ctx.lineTo(lx + params.x, ly + params.y)
          l += 1
        } else {
          // Otherwise, stop drawing
          break
        }
      }
      l -= 1
      // Optionally add arrows to path
      _addEndArrow(
        canvas,
        ctx,
        params,
        path,
        path[`x${l - 1}`] + params.x,
        path[`y${l - 1}`] + params.y,
        path[`x${l}`] + params.x,
        path[`y${l}`] + params.y
      )
    }

    // Draws line
    $.fn.drawLine = function drawLine(args) {
      const $canvases = this
      let e
      let ctx
      let params

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawLine)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params)
            _setGlobalProps($canvases[e], ctx, params)

            // Draw each point
            ctx.beginPath()
            _drawLine($canvases[e], ctx, params, params)
            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Optionally close path
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    // Draws quadratic curve (internal)
    function _drawQuadratic(canvas, ctx, params, path) {
      let l
      let lx
      let ly
      let lcx
      let lcy

      l = 2

      _addStartArrow(
        canvas,
        ctx,
        params,
        path,
        path.cx1 + params.x,
        path.cy1 + params.y,
        path.x1 + params.x,
        path.y1 + params.y
      )

      if (path.x1 !== undefined && path.y1 !== undefined) {
        ctx.moveTo(path.x1 + params.x, path.y1 + params.y)
      }
      while (true) {
        // Calculate next coordinates
        lx = path[`x${l}`]
        ly = path[`y${l}`]
        lcx = path[`cx${l - 1}`]
        lcy = path[`cy${l - 1}`]
        // If coordinates are given
        if (lx !== undefined && ly !== undefined && lcx !== undefined && lcy !== undefined) {
          // Draw next curve
          ctx.quadraticCurveTo(lcx + params.x, lcy + params.y, lx + params.x, ly + params.y)
          l += 1
        } else {
          // Otherwise, stop drawing
          break
        }
      }
      l -= 1
      _addEndArrow(
        canvas,
        ctx,
        params,
        path,
        path[`cx${l - 1}`] + params.x,
        path[`cy${l - 1}`] + params.y,
        path[`x${l}`] + params.x,
        path[`y${l}`] + params.y
      )
    }

    // Draws quadratic curve
    $.fn.drawQuadratic = function drawQuadratic(args) {
      const $canvases = this
      let e
      let ctx
      let params

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawQuadratic)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params)
            _setGlobalProps($canvases[e], ctx, params)

            // Draw each point
            ctx.beginPath()
            _drawQuadratic($canvases[e], ctx, params, params)
            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Optionally close path
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    // Draws Bezier curve (internal)
    function _drawBezier(canvas, ctx, params, path) {
      let l
      let lc
      let lx
      let ly
      let lcx1
      let lcy1
      let lcx2
      let lcy2

      l = 2
      lc = 1

      _addStartArrow(
        canvas,
        ctx,
        params,
        path,
        path.cx1 + params.x,
        path.cy1 + params.y,
        path.x1 + params.x,
        path.y1 + params.y
      )

      if (path.x1 !== undefined && path.y1 !== undefined) {
        ctx.moveTo(path.x1 + params.x, path.y1 + params.y)
      }
      while (true) {
        // Calculate next coordinates
        lx = path[`x${l}`]
        ly = path[`y${l}`]
        lcx1 = path[`cx${lc}`]
        lcy1 = path[`cy${lc}`]
        lcx2 = path[`cx${lc + 1}`]
        lcy2 = path[`cy${lc + 1}`]
        // If next coordinates are given
        if (
          lx !== undefined &&
          ly !== undefined &&
          lcx1 !== undefined &&
          lcy1 !== undefined &&
          lcx2 !== undefined &&
          lcy2 !== undefined
        ) {
          // Draw next curve
          ctx.bezierCurveTo(
            lcx1 + params.x,
            lcy1 + params.y,
            lcx2 + params.x,
            lcy2 + params.y,
            lx + params.x,
            ly + params.y
          )
          l += 1
          lc += 2
        } else {
          // Otherwise, stop drawing
          break
        }
      }
      l -= 1
      lc -= 2
      _addEndArrow(
        canvas,
        ctx,
        params,
        path,
        path[`cx${lc + 1}`] + params.x,
        path[`cy${lc + 1}`] + params.y,
        path[`x${l}`] + params.x,
        path[`y${l}`] + params.y
      )
    }

    // Draws Bezier curve
    $.fn.drawBezier = function drawBezier(args) {
      const $canvases = this
      let e
      let ctx
      let params

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawBezier)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params)
            _setGlobalProps($canvases[e], ctx, params)

            // Draw each point
            ctx.beginPath()
            _drawBezier($canvases[e], ctx, params, params)
            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Optionally close path
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    // Retrieves the x-coordinate for the given vector angle and length
    function _getVectorX(params, angle, length) {
      angle *= params._toRad
      angle -= PI / 2
      return length * cos(angle)
    }
    // Retrieves the y-coordinate for the given vector angle and length
    function _getVectorY(params, angle, length) {
      angle *= params._toRad
      angle -= PI / 2
      return length * sin(angle)
    }

    // Draws vector (internal) #2
    function _drawVector(canvas, ctx, params, path) {
      let l
      let angle
      let length
      let offsetX
      let offsetY
      let x
      let y
      let x3
      let y3
      let x4
      let y4

      // Determine offset from dragging
      if (params === path) {
        offsetX = 0
        offsetY = 0
      } else {
        offsetX = params.x
        offsetY = params.y
      }

      l = 1
      x = x3 = x4 = path.x + offsetX
      y = y3 = y4 = path.y + offsetY

      _addStartArrow(
        canvas,
        ctx,
        params,
        path,
        x + _getVectorX(params, path.a1, path.l1),
        y + _getVectorY(params, path.a1, path.l1),
        x,
        y
      )

      // The vector starts at the given (x, y) coordinates
      if (path.x !== undefined && path.y !== undefined) {
        ctx.moveTo(x, y)
      }
      while (true) {
        angle = path[`a${l}`]
        length = path[`l${l}`]

        if (angle !== undefined && length !== undefined) {
          // Convert the angle to radians with 0 degrees starting at north
          // Keep track of last two coordinates
          x3 = x4
          y3 = y4
          // Compute (x, y) coordinates from angle and length
          x4 += _getVectorX(params, angle, length)
          y4 += _getVectorY(params, angle, length)
          ctx.lineTo(x4, y4)
          l += 1
        } else {
          // Otherwise, stop drawing
          break
        }
      }
      _addEndArrow(canvas, ctx, params, path, x3, y3, x4, y4)
    }

    // Draws vector
    $.fn.drawVector = function drawVector(args) {
      const $canvases = this
      let e
      let ctx
      let params

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawVector)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params)
            _setGlobalProps($canvases[e], ctx, params)

            // Draw each point
            ctx.beginPath()
            _drawVector($canvases[e], ctx, params, params)
            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Optionally close path
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    // Draws a path consisting of one or more subpaths
    $.fn.drawPath = function drawPath(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let l
      let lp

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawPath)
          if (params.visible) {
            _transformShape($canvases[e], ctx, params)
            _setGlobalProps($canvases[e], ctx, params)

            ctx.beginPath()
            l = 1
            while (true) {
              lp = params[`p${l}`]
              if (lp !== undefined) {
                lp = new jCanvasObject(lp)
                if (lp.type === 'line') {
                  _drawLine($canvases[e], ctx, params, lp)
                } else if (lp.type === 'quadratic') {
                  _drawQuadratic($canvases[e], ctx, params, lp)
                } else if (lp.type === 'bezier') {
                  _drawBezier($canvases[e], ctx, params, lp)
                } else if (lp.type === 'vector') {
                  _drawVector($canvases[e], ctx, params, lp)
                } else if (lp.type === 'arc') {
                  _drawArc($canvases[e], ctx, params, lp)
                }
                l += 1
              } else {
                break
              }
            }

            // Check for jCanvas events
            _detectEvents($canvases[e], ctx, params)
            // Optionally close path
            _closePath($canvases[e], ctx, params)
          }
        }
      }
      return $canvases
    }

    /* Text API */

    // Calculates font string and set it as the canvas font
    function _setCanvasFont(canvas, ctx, params) {
      // Otherwise, use the given font attributes
      if (!isNaN(Number(params.fontSize))) {
        // Give font size units if it doesn't have any
        params.fontSize += 'px'
      }
      // Set font using given font properties
      ctx.font = `${params.fontStyle} ${params.fontSize} ${params.fontFamily}`
    }

    // Measures canvas text
    function _measureText(canvas, ctx, params, lines) {
      let originalSize
      let curWidth
      let l
      const { propCache } = caches

      // Used cached width/height if possible
      if (
        propCache.text === params.text &&
        propCache.fontStyle === params.fontStyle &&
        propCache.fontSize === params.fontSize &&
        propCache.fontFamily === params.fontFamily &&
        propCache.maxWidth === params.maxWidth &&
        propCache.lineHeight === params.lineHeight
      ) {
        params.width = propCache.width
        params.height = propCache.height
      } else {
        // Calculate text dimensions only once

        // Calculate width of first line (for comparison)
        params.width = ctx.measureText(lines[0]).width

        // Get width of longest line
        for (l = 1; l < lines.length; l += 1) {
          curWidth = ctx.measureText(lines[l]).width
          // Ensure text's width is the width of its longest line
          if (curWidth > params.width) {
            params.width = curWidth
          }
        }

        // Save original font size
        originalSize = canvas.style.fontSize
        // Temporarily set canvas font size to retrieve size in pixels
        canvas.style.fontSize = params.fontSize
        // Save text width and height in parameters object
        params.height = parseFloat($.css(canvas, 'fontSize')) * lines.length * params.lineHeight
        // Reset font size to original size
        canvas.style.fontSize = originalSize
      }
    }

    // Wraps a string of text within a defined width
    function _wrapText(ctx, params) {
      const allText = String(params.text)
      // Maximum line width (optional)
      const { maxWidth } = params
      // Lines created by manual line breaks (\n)
      const manualLines = allText.split('\n')
      // All lines created manually and by wrapping
      let allLines = []
      // Other variables
      let lines
      let line
      let l
      let text
      let words
      let w

      // Loop through manually-broken lines
      for (l = 0; l < manualLines.length; l += 1) {
        text = manualLines[l]
        // Split line into list of words
        words = text.split(' ')
        lines = []
        line = ''

        // If text is short enough initially
        // Or, if the text consists of only one word
        if (words.length === 1 || ctx.measureText(text).width < maxWidth) {
          // No need to wrap text
          lines = [text]
        } else {
          // Wrap lines
          for (w = 0; w < words.length; w += 1) {
            // Once line gets too wide, push word to next line
            if (ctx.measureText(line + words[w]).width > maxWidth) {
              // This check prevents empty lines from being created
              if (line !== '') {
                lines.push(line)
              }
              // Start new line and repeat process
              line = ''
            }
            // Add words to line until the line is too wide
            line += words[w]
            // Do not add a space after the last word
            if (w !== words.length - 1) {
              line += ' '
            }
          }
          // The last word should always be pushed
          lines.push(line)
        }
        // Remove extra space at the end of each line
        allLines = allLines.concat(
          lines
            .join('\n')
            .replace(/((\n))|($)/gi, '$2')
            .split('\n')
        )
      }

      return allLines
    }

    // Draws text on canvas
    $.fn.drawText = function drawText(args) {
      const $canvases = this
      let e
      let ctx
      let params
      let layer
      let lines
      let line
      let l
      let fontSize
      const constantCloseness = 500
      let nchars
      let chars
      let ch
      let c
      let x
      let y

      for (e = 0; e < $canvases.length; e += 1) {
        ctx = _getContext($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer($canvases[e], params, args, drawText)
          if (params.visible) {
            // Set text-specific properties
            ctx.textBaseline = params.baseline
            ctx.textAlign = params.align

            // Set canvas font using given properties
            _setCanvasFont($canvases[e], ctx, params)

            if (params.maxWidth !== null) {
              // Wrap text using an internal function
              lines = _wrapText(ctx, params)
            } else {
              // Convert string of text to list of lines
              lines = params.text.toString().split('\n')
            }

            // Calculate text's width and height
            _measureText($canvases[e], ctx, params, lines)

            // If text is a layer
            if (layer) {
              // Copy calculated width/height to layer object
              layer.width = params.width
              layer.height = params.height
            }

            _transformShape($canvases[e], ctx, params, params.width, params.height)
            _setGlobalProps($canvases[e], ctx, params)

            // Adjust text position to accomodate different horizontal alignments
            x = params.x
            if (params.align === 'left') {
              if (params.respectAlign) {
                // Realign text to the left if chosen
                params.x += params.width / 2
              } else {
                // Center text block by default
                x -= params.width / 2
              }
            } else if (params.align === 'right') {
              if (params.respectAlign) {
                // Realign text to the right if chosen
                params.x -= params.width / 2
              } else {
                // Center text block by default
                x += params.width / 2
              }
            }

            if (params.radius) {
              fontSize = parseFloat(params.fontSize)

              // Greater values move clockwise
              if (params.letterSpacing === null) {
                params.letterSpacing = fontSize / constantCloseness
              }

              // Loop through each line of text
              for (l = 0; l < lines.length; l += 1) {
                ctx.save()
                ctx.translate(params.x, params.y)
                line = lines[l]
                if (params.flipArcText) {
                  chars = line.split('')
                  chars.reverse()
                  line = chars.join('')
                }
                nchars = line.length
                ctx.rotate(-(PI * params.letterSpacing * (nchars - 1)) / 2)
                // Loop through characters on each line
                for (c = 0; c < nchars; c += 1) {
                  ch = line[c]
                  // If character is not the first character
                  if (c !== 0) {
                    // Rotate character onto arc
                    ctx.rotate(PI * params.letterSpacing)
                  }
                  ctx.save()
                  ctx.translate(0, -params.radius)
                  if (params.flipArcText) {
                    ctx.scale(-1, -1)
                  }
                  ctx.fillText(ch, 0, 0)
                  // Prevent extra shadow created by stroke (but only when fill is present)
                  if (params.fillStyle !== 'transparent') {
                    ctx.shadowColor = 'transparent'
                  }
                  if (params.strokeWidth !== 0) {
                    // Only stroke if the stroke is not 0
                    ctx.strokeText(ch, 0, 0)
                  }
                  ctx.restore()
                }
                params.radius -= fontSize
                params.letterSpacing += fontSize / (constantCloseness * 2 * PI)
                ctx.restore()
              }
            } else {
              // Draw each line of text separately
              for (l = 0; l < lines.length; l += 1) {
                line = lines[l]
                // Add line offset to center point, but subtract some to center everything
                y =
                  params.y +
                  (l * params.height) / lines.length -
                  ((lines.length - 1) * params.height) / lines.length / 2

                ctx.shadowColor = params.shadowColor

                // Fill & stroke text
                ctx.fillText(line, x, y)
                // Prevent extra shadow created by stroke (but only when fill is present)
                if (params.fillStyle !== 'transparent') {
                  ctx.shadowColor = 'transparent'
                }
                if (params.strokeWidth !== 0) {
                  // Only stroke if the stroke is not 0
                  ctx.strokeText(line, x, y)
                }
              }
            }

            // Adjust bounding box according to text baseline
            y = 0
            if (params.baseline === 'top') {
              y += params.height / 2
            } else if (params.baseline === 'bottom') {
              y -= params.height / 2
            }

            // Detect jCanvas events
            if (params._event) {
              ctx.beginPath()
              ctx.rect(
                params.x - params.width / 2,
                params.y - params.height / 2 + y,
                params.width,
                params.height
              )
              _detectEvents($canvases[e], ctx, params)
              // Close path and configure masking
              ctx.closePath()
            }
            _restoreTransform(ctx, params)
          }
        }
      }
      // Cache jCanvas parameters object for efficiency
      caches.propCache = params
      return $canvases
    }

    // Measures text width/height using the given parameters
    $.fn.measureText = function measureText(args) {
      const $canvases = this
      let ctx
      let params
      let lines

      // Attempt to retrieve layer
      params = $canvases.getLayer(args)
      // If layer does not exist or if returned object is not a jCanvas layer
      if (!params || (params && !params._layer)) {
        params = new jCanvasObject(args)
      }

      ctx = _getContext($canvases[0])
      if (ctx) {
        // Set canvas font using given properties
        _setCanvasFont($canvases[0], ctx, params)
        // Calculate width and height of text
        if (params.maxWidth !== null) {
          lines = _wrapText(ctx, params)
        } else {
          lines = params.text.split('\n')
        }
        _measureText($canvases[0], ctx, params, lines)
      }

      return params
    }

    /* Image API */

    // Draws image on canvas
    $.fn.drawImage = function drawImage(args) {
      const $canvases = this
      let canvas
      let e
      let ctx
      let data
      let params
      let layer
      let img
      let imgCtx
      let source
      const { imageCache } = caches

      // Draw image function
      // eslint-disable-next-line func-style
      function draw(canvas, ctx, data, params, layer) {
        // If width and sWidth are not defined, use image width
        if (params.width === null && params.sWidth === null) {
          params.width = params.sWidth = img.width
        }
        // If width and sHeight are not defined, use image height
        if (params.height === null && params.sHeight === null) {
          params.height = params.sHeight = img.height
        }

        // Ensure image layer's width and height are accurate
        if (layer) {
          layer.width = params.width
          layer.height = params.height
        }

        // Only crop image if all cropping properties are given
        if (
          params.sWidth !== null &&
          params.sHeight !== null &&
          params.sx !== null &&
          params.sy !== null
        ) {
          // If width is not defined, use the given sWidth
          if (params.width === null) {
            params.width = params.sWidth
          }
          // If height is not defined, use the given sHeight
          if (params.height === null) {
            params.height = params.sHeight
          }

          // Optionally crop from top-left corner of region
          if (params.cropFromCenter) {
            params.sx += params.sWidth / 2
            params.sy += params.sHeight / 2
          }

          // Ensure cropped region does not escape image boundaries

          // Top
          if (params.sy - params.sHeight / 2 < 0) {
            params.sy = params.sHeight / 2
          }
          // Bottom
          if (params.sy + params.sHeight / 2 > img.height) {
            params.sy = img.height - params.sHeight / 2
          }
          // Left
          if (params.sx - params.sWidth / 2 < 0) {
            params.sx = params.sWidth / 2
          }
          // Right
          if (params.sx + params.sWidth / 2 > img.width) {
            params.sx = img.width - params.sWidth / 2
          }

          _transformShape(canvas, ctx, params, params.width, params.height)
          _setGlobalProps(canvas, ctx, params)

          // Draw image
          ctx.drawImage(
            img,
            params.sx - params.sWidth / 2,
            params.sy - params.sHeight / 2,
            params.sWidth,
            params.sHeight,
            params.x - params.width / 2,
            params.y - params.height / 2,
            params.width,
            params.height
          )
        } else {
          // Show entire image if no crop region is defined
          _transformShape(canvas, ctx, params, params.width, params.height)
          _setGlobalProps(canvas, ctx, params)

          // Draw image on canvas
          ctx.drawImage(
            img,
            params.x - params.width / 2,
            params.y - params.height / 2,
            params.width,
            params.height
          )
        }

        // Draw invisible rectangle to allow for events and masking
        ctx.beginPath()
        ctx.rect(
          params.x - params.width / 2,
          params.y - params.height / 2,
          params.width,
          params.height
        )
        // Check for jCanvas events
        _detectEvents(canvas, ctx, params)
        // Close path and configure masking
        ctx.closePath()
        _restoreTransform(ctx, params)
        _enableMasking(ctx, data, params)
      }
      // On load function
      function onload(canvas, ctx, data, params, layer) {
        return function() {
          const $canvas = $(canvas)
          draw(canvas, ctx, data, params, layer)
          if (params.layer) {
            // Trigger 'load' event for layers
            _triggerLayerEvent($canvas, data, layer, 'load')
          } else if (params.load) {
            // Run 'load' callback for non-layers
            params.load.call($canvas[0], layer)
          }
          // Continue drawing successive layers after this image layer has loaded
          if (params.layer) {
            // Store list of previous masks for each layer
            layer._masks = data.transforms.masks.slice(0)
            if (params._next) {
              // Draw successive layers
              const complete = data.drawLayersComplete
              delete data.drawLayersComplete
              $canvas.drawLayers({
                clear: false,
                resetFire: true,
                index: params._next,
                complete
              })
            }
          }
        }
      }
      for (e = 0; e < $canvases.length; e += 1) {
        canvas = $canvases[e]
        ctx = _getContext($canvases[e])
        if (ctx) {
          data = _getCanvasData($canvases[e])
          params = new jCanvasObject(args)
          layer = _addLayer($canvases[e], params, args, drawImage)
          if (params.visible) {
            // Cache the given source
            source = params.source

            imgCtx = source.getContext
            if (source.src || imgCtx) {
              // Use image or canvas element if given
              img = source
            } else if (source) {
              if (imageCache[source] && imageCache[source].complete) {
                // Get the image element from the cache if possible
                img = imageCache[source]
              } else {
                // Otherwise, get the image from the given source URL
                img = new Image()
                // If source URL is not a data URL
                if (!source.match(/^data:/i)) {
                  // Set crossOrigin for this image
                  img.crossOrigin = params.crossOrigin
                }
                img.src = source
                // Save image in cache for improved performance
                imageCache[source] = img
              }
            }

            if (img) {
              if (img.complete || imgCtx) {
                // Draw image if already loaded
                onload(canvas, ctx, data, params, layer)()
              } else {
                // Otherwise, draw image when it loads
                img.onload = onload(canvas, ctx, data, params, layer)
                // Fix onload() bug in IE9
                img.src = img.src
              }
            }
          }
        }
      }
      return $canvases
    }

    // Creates a canvas pattern object
    $.fn.createPattern = function createPattern(args) {
      const $canvases = this
      let ctx
      let params
      let img
      let imgCtx
      let pattern
      let source

      // Function to be called when pattern loads
      function onload() {
        // Create pattern
        pattern = ctx.createPattern(img, params.repeat)
        // Run callback function if defined
        if (params.load) {
          params.load.call($canvases[0], pattern)
        }
      }

      ctx = _getContext($canvases[0])
      if (ctx) {
        params = new jCanvasObject(args)

        // Cache the given source
        source = params.source

        // Draw when image is loaded (if load() callback function is defined)

        if (isFunction(source)) {
          // Draw pattern using function if given

          img = $('<canvas />')[0]
          img.width = params.width
          img.height = params.height
          imgCtx = _getContext(img)
          source.call(img, imgCtx)
          onload()
        } else {
          // Otherwise, draw pattern using source image

          imgCtx = source.getContext
          if (source.src || imgCtx) {
            // Use image element if given
            img = source
          } else {
            // Use URL if given to get the image
            img = new Image()
            // If source URL is not a data URL
            if (!source.match(/^data:/i)) {
              // Set crossOrigin for this image
              img.crossOrigin = params.crossOrigin
            }
            img.src = source
          }

          // Create pattern if already loaded
          if (img.complete || imgCtx) {
            onload()
          } else {
            img.onload = onload
            // Fix onload() bug in IE9
            img.src = img.src
          }
        }
      } else {
        pattern = null
      }
      return pattern
    }

    // Creates a canvas gradient object
    $.fn.createGradient = function createGradient(args) {
      const $canvases = this
      let ctx
      let params
      let gradient
      const stops = []
      let nstops
      let start
      let end
      let i
      let a
      let n
      let p

      params = new jCanvasObject(args)
      ctx = _getContext($canvases[0])
      if (ctx) {
        // Gradient coordinates must be defined
        params.x1 = params.x1 || 0
        params.y1 = params.y1 || 0
        params.x2 = params.x2 || 0
        params.y2 = params.y2 || 0

        if (params.r1 !== null && params.r2 !== null) {
          // Create radial gradient if chosen
          gradient = ctx.createRadialGradient(
            params.x1,
            params.y1,
            params.r1,
            params.x2,
            params.y2,
            params.r2
          )
        } else {
          // Otherwise, create a linear gradient by default
          gradient = ctx.createLinearGradient(params.x1, params.y1, params.x2, params.y2)
        }

        // Count number of color stops
        for (i = 1; params[`c${i}`] !== undefined; i += 1) {
          if (params[`s${i}`] !== undefined) {
            stops.push(params[`s${i}`])
          } else {
            stops.push(null)
          }
        }
        nstops = stops.length

        // Define start stop if not already defined
        if (stops[0] === null) {
          stops[0] = 0
        }
        // Define end stop if not already defined
        if (stops[nstops - 1] === null) {
          stops[nstops - 1] = 1
        }

        // Loop through color stops to fill in the blanks
        for (i = 0; i < nstops; i += 1) {
          // A progression, in this context, is defined as all of the color stops between and including two known color stops

          if (stops[i] !== null) {
            // Start a new progression if stop is a number

            // Number of stops in current progression
            n = 1
            // Current iteration in current progression
            p = 0
            start = stops[i]

            // Look ahead to find end stop
            for (a = i + 1; a < nstops; a += 1) {
              if (stops[a] !== null) {
                // If this future stop is a number, make it the end stop for this progression
                end = stops[a]
                break
              } else {
                // Otherwise, keep looking ahead
                n += 1
              }
            }

            // Ensure start stop is not greater than end stop
            if (start > end) {
              stops[a] = stops[i]
            }
          } else if (stops[i] === null) {
            // Calculate stop if not initially given
            p += 1
            stops[i] = start + p * ((end - start) / n)
          }
          // Add color stop to gradient object
          gradient.addColorStop(stops[i], params[`c${i + 1}`])
        }
      } else {
        gradient = null
      }
      return gradient
    }

    // Manipulates pixels on the canvas
    $.fn.setPixels = function setPixels(args) {
      const $canvases = this
      let canvas
      let e
      let ctx
      let canvasData
      let params
      let px
      let imgData
      let pixelData
      let i
      let len

      for (e = 0; e < $canvases.length; e += 1) {
        canvas = $canvases[e]
        ctx = _getContext(canvas)
        canvasData = _getCanvasData($canvases[e])
        if (ctx) {
          params = new jCanvasObject(args)
          _addLayer(canvas, params, args, setPixels)
          _transformShape($canvases[e], ctx, params, params.width, params.height)

          // Use entire canvas of x, y, width, or height is not defined
          if (params.width === null || params.height === null) {
            params.width = canvas.width
            params.height = canvas.height
            params.x = params.width / 2
            params.y = params.height / 2
          }

          if (params.width !== 0 && params.height !== 0) {
            // Only set pixels if width and height are not zero

            imgData = ctx.getImageData(
              (params.x - params.width / 2) * canvasData.pixelRatio,
              (params.y - params.height / 2) * canvasData.pixelRatio,
              params.width * canvasData.pixelRatio,
              params.height * canvasData.pixelRatio
            )
            pixelData = imgData.data
            len = pixelData.length

            // Loop through pixels with the "each" callback function
            if (params.each) {
              for (i = 0; i < len; i += 4) {
                px = {
                  r: pixelData[i],
                  g: pixelData[i + 1],
                  b: pixelData[i + 2],
                  a: pixelData[i + 3]
                }
                params.each.call(canvas, px, params)
                pixelData[i] = px.r
                pixelData[i + 1] = px.g
                pixelData[i + 2] = px.b
                pixelData[i + 3] = px.a
              }
            }
            // Put pixels on canvas
            ctx.putImageData(
              imgData,
              (params.x - params.width / 2) * canvasData.pixelRatio,
              (params.y - params.height / 2) * canvasData.pixelRatio
            )
            // Restore transformation
            ctx.restore()
          }
        }
      }
      return $canvases
    }

    // Retrieves canvas image as data URL
    $.fn.getCanvasImage = function getCanvasImage(type, quality) {
      const $canvases = this
      let canvas
      let dataURL = null
      if ($canvases.length !== 0) {
        canvas = $canvases[0]
        if (canvas.toDataURL) {
          // JPEG quality defaults to 1
          if (quality === undefined) {
            quality = 1
          }
          dataURL = canvas.toDataURL(`image/${type}`, quality)
        }
      }
      return dataURL
    }

    // Scales canvas based on the device's pixel ratio
    $.fn.detectPixelRatio = function detectPixelRatio(callback) {
      const $canvases = this
      let canvas
      let e
      let ctx
      let devicePixelRatio
      let backingStoreRatio
      let ratio
      let oldWidth
      let oldHeight
      let data

      for (e = 0; e < $canvases.length; e += 1) {
        // Get canvas and its associated data
        canvas = $canvases[e]
        ctx = _getContext(canvas)
        data = _getCanvasData($canvases[e])

        // If canvas has not already been scaled with this method
        if (!data.scaled) {
          // Determine device pixel ratios
          devicePixelRatio = window.devicePixelRatio || 1
          backingStoreRatio =
            ctx.webkitBackingStorePixelRatio ||
            ctx.mozBackingStorePixelRatio ||
            ctx.msBackingStorePixelRatio ||
            ctx.oBackingStorePixelRatio ||
            ctx.backingStorePixelRatio ||
            1

          // Calculate general ratio based on the two given ratios
          ratio = devicePixelRatio / backingStoreRatio

          if (ratio !== 1) {
            // Scale canvas relative to ratio

            // Get the current canvas dimensions for future use
            oldWidth = canvas.width
            oldHeight = canvas.height

            // Resize canvas relative to the determined ratio
            canvas.width = oldWidth * ratio
            canvas.height = oldHeight * ratio

            // Scale canvas back to original dimensions via CSS
            canvas.style.width = `${oldWidth}px`
            canvas.style.height = `${oldHeight}px`

            // Scale context to counter the manual scaling of canvas
            ctx.scale(ratio, ratio)
          }

          // Set pixel ratio on canvas data object
          data.pixelRatio = ratio
          // Ensure that this method can only be called once for any given canvas
          data.scaled = true

          // Call the given callback function with the ratio as its only argument
          if (callback) {
            callback.call(canvas, ratio)
          }
        }
      }
      return $canvases
    }

    // Clears the jCanvas cache
    jCanvas.clearCache = function clearCache() {
      let cacheName
      for (cacheName in caches) {
        if (Object.prototype.hasOwnProperty.call(caches, cacheName)) {
          caches[cacheName] = {}
        }
      }
    }

    // Enable canvas feature detection with $.support
    $.support.canvas = $('<canvas />')[0].getContext !== undefined

    // Export jCanvas functions
    extendObject(jCanvas, {
      defaults,
      setGlobalProps: _setGlobalProps,
      transformShape: _transformShape,
      detectEvents: _detectEvents,
      closePath: _closePath,
      setCanvasFont: _setCanvasFont,
      measureText: _measureText
    })
    $.jCanvas = jCanvas
    $.jCanvasObject = jCanvasObject
  }
)
